@agent-native/core 0.153.1 → 0.153.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.
Files changed (41) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/adhoc-analysis/SKILL.md +21 -19
  3. package/corpus/templates/analytics/AGENTS.md +4 -0
  4. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -0
  5. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +0 -11
  6. package/corpus/templates/clips/app/components/player/share-dialog.tsx +32 -33
  7. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +29 -39
  8. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +0 -1
  9. package/corpus/templates/clips/desktop/src/app.tsx +817 -441
  10. package/corpus/templates/clips/desktop/src/styles.css +234 -0
  11. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
  12. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +593 -7
  13. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +95 -10
  14. package/corpus/templates/forms/AGENTS.md +6 -0
  15. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -0
  16. package/corpus/templates/mail/AGENTS.md +5 -4
  17. package/corpus/templates/mail/actions/get-automation-settings.ts +17 -6
  18. package/corpus/templates/mail/actions/send-email.ts +17 -9
  19. package/corpus/templates/mail/actions/send-queued-drafts.ts +21 -11
  20. package/corpus/templates/mail/actions/update-automation-settings.ts +20 -6
  21. package/corpus/templates/mail/app/i18n/en-US.ts +5 -0
  22. package/corpus/templates/mail/app/pages/SettingsPage.tsx +68 -7
  23. package/corpus/templates/mail/server/lib/automation-settings.ts +27 -0
  24. package/corpus/templates/mail/server/plugins/agent-chat.ts +2 -0
  25. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +3 -3
  26. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +17 -4
  27. package/corpus/templates/slides/app/context/DeckContext.tsx +3 -2
  28. package/corpus/templates/slides/app/pages/DeckEditor.tsx +44 -0
  29. package/corpus/templates/slides/changelog/2026-08-12-arrow-key-navigation-in-the-slide-film-strip-now-scrolls-sli.md +6 -0
  30. package/corpus/templates/slides/changelog/2026-08-12-duplicate-a-slide-from-the-film-strip-with-cmd-ctrl-c-then-c.md +6 -0
  31. package/corpus/templates/slides/changelog/2026-08-12-presenting-a-deck-now-returns-you-to-the-slide-you-were-view.md +6 -0
  32. package/dist/client/resources/mcp-integration-catalog.d.ts +7 -0
  33. package/dist/client/resources/mcp-integration-catalog.js +14 -1
  34. package/dist/client/settings/AgentsSection.js +46 -30
  35. package/dist/client/sharing/ShareDialog.js +4 -2
  36. package/dist/collab/awareness.d.ts +2 -2
  37. package/dist/observability/routes.d.ts +5 -5
  38. package/dist/secrets/routes.d.ts +9 -9
  39. package/dist/server/agent-chat-plugin.js +1 -0
  40. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  41. package/package.json +1 -1
@@ -136,6 +136,8 @@ pub(crate) fn rewind_clip_prepare(
136
136
  let sources = screen_memory::rewind_clip_sources(&app);
137
137
  let output = artifact_path(&app, &artifact_label)?;
138
138
  #[cfg(target_os = "macos")]
139
+ let recovery_intent = (server_url.clone(), recording_id.clone());
140
+ #[cfg(target_os = "macos")]
139
141
  let shared_sink = match screen_memory::prepare_shared_clip_sink(
140
142
  &app,
141
143
  output,
@@ -168,6 +170,29 @@ pub(crate) fn rewind_clip_prepare(
168
170
  release_temporary_audio(&app, temporary_audio);
169
171
  return Err(not_compatible("shared Rewind Clip sinks require macOS"));
170
172
  }
173
+ #[cfg(target_os = "macos")]
174
+ if let (Some(server_url), Some(recording_id)) = recovery_intent {
175
+ if !server_url.trim().is_empty() && !recording_id.trim().is_empty() {
176
+ let (width, height) = shared_sink.dimensions();
177
+ if let Err(error) = native_screen::persist_recording_intent(
178
+ shared_sink.path(),
179
+ &recording_id,
180
+ &server_url,
181
+ native_screen::MP4_RECORDING_MIME_TYPE,
182
+ Some(width),
183
+ Some(height),
184
+ include_mic || include_system_audio,
185
+ include_mic,
186
+ include_system_audio,
187
+ has_camera,
188
+ true,
189
+ ) {
190
+ shared_sink.cancel();
191
+ release_temporary_audio(&app, temporary_audio);
192
+ return Err(error);
193
+ }
194
+ }
195
+ }
171
196
  let response_sources = sources.clone();
172
197
  let mut active = state.0.lock().map_err(|error| error.to_string())?;
173
198
  if active.is_some() {
@@ -1013,6 +1038,7 @@ fn materialize(
1013
1038
  label: &str,
1014
1039
  include_mic: bool,
1015
1040
  include_system_audio: bool,
1041
+ recovery: Option<(&str, &str, bool)>,
1016
1042
  ) -> Result<FinalizedNativeArtifact, String> {
1017
1043
  let active = take_active(state)?;
1018
1044
  let mut intervals = active.intervals.clone();
@@ -1087,10 +1113,25 @@ fn materialize(
1087
1113
  }
1088
1114
  let output = artifact_path(app, label)?;
1089
1115
  let audio = select_audio(&active.sources, include_mic, include_system_audio)?;
1090
- native_screen::materialize_mp4_slices_exact(&slices, &output, audio)?;
1091
1116
  let first = first_segment
1092
1117
  .as_ref()
1093
1118
  .ok_or_else(|| "no Rewind media selected".to_string())?;
1119
+ if let Some((server_url, recording_id, has_camera)) = recovery {
1120
+ native_screen::persist_recording_intent(
1121
+ &output,
1122
+ recording_id,
1123
+ server_url,
1124
+ native_screen::MP4_RECORDING_MIME_TYPE,
1125
+ first.width,
1126
+ first.height,
1127
+ include_mic || include_system_audio,
1128
+ include_mic,
1129
+ include_system_audio,
1130
+ has_camera,
1131
+ true,
1132
+ )?;
1133
+ }
1134
+ native_screen::materialize_mp4_slices_exact(&slices, &output, audio)?;
1094
1135
  Ok(FinalizedNativeArtifact::rewind_mp4(
1095
1136
  output,
1096
1137
  duration_ms,
@@ -1174,8 +1215,8 @@ pub(crate) async fn rewind_clip_stop_and_upload(
1174
1215
  &server_url,
1175
1216
  &recording_id,
1176
1217
  logical_duration_ms as u128,
1177
- sink_width,
1178
- sink_height,
1218
+ Some(sink_width),
1219
+ Some(sink_height),
1179
1220
  include_mic,
1180
1221
  include_system_audio,
1181
1222
  has_camera,
@@ -1205,8 +1246,8 @@ pub(crate) async fn rewind_clip_stop_and_upload(
1205
1246
  &server_url,
1206
1247
  &recording_id,
1207
1248
  result.duration_ms as u128,
1208
- result.width,
1209
- result.height,
1249
+ Some(result.width),
1250
+ Some(result.height),
1210
1251
  include_mic,
1211
1252
  include_system_audio,
1212
1253
  has_camera,
@@ -1232,8 +1273,8 @@ pub(crate) async fn rewind_clip_stop_and_upload(
1232
1273
  &server_url,
1233
1274
  &recording_id,
1234
1275
  result.duration_ms as u128,
1235
- result.width,
1236
- result.height,
1276
+ Some(result.width),
1277
+ Some(result.height),
1237
1278
  include_mic,
1238
1279
  include_system_audio,
1239
1280
  has_camera,
@@ -1289,19 +1330,62 @@ pub(crate) async fn rewind_clip_stop_and_upload(
1289
1330
  &recording_id,
1290
1331
  include_mic,
1291
1332
  include_system_audio,
1333
+ Some((&server_url, &recording_id, has_camera)),
1292
1334
  )?;
1293
- native_screen::upload_finalized_native_artifact(
1335
+ native_screen::persist_shared_clip_recording(
1336
+ &app,
1337
+ &artifact.path,
1338
+ &server_url,
1339
+ &recording_id,
1340
+ artifact.duration_ms,
1341
+ artifact.width,
1342
+ artifact.height,
1343
+ include_mic,
1344
+ include_system_audio,
1345
+ has_camera,
1346
+ None,
1347
+ )?;
1348
+ let recovery_server_url = server_url.clone();
1349
+ let recovery_recording_id = recording_id.clone();
1350
+ let artifact_path = artifact.path.clone();
1351
+ let result = native_screen::upload_finalized_native_artifact(
1294
1352
  &app,
1295
1353
  &artifact,
1296
1354
  server_url,
1297
- recording_id,
1355
+ recording_id.clone(),
1298
1356
  auth_token.unwrap_or_default(),
1299
1357
  cookie.unwrap_or_default(),
1300
1358
  NativeUploadMode::from_option(upload_mode),
1301
1359
  include_mic || include_system_audio,
1302
1360
  has_camera,
1303
1361
  )
1304
- .await
1362
+ .await;
1363
+ match &result {
1364
+ Ok(upload) if !upload.verification_pending => {
1365
+ native_screen::clear_shared_clip_recording(
1366
+ &app,
1367
+ &recovery_recording_id,
1368
+ &artifact_path,
1369
+ );
1370
+ }
1371
+ Err(error) => {
1372
+ let _ = native_screen::persist_shared_clip_recording(
1373
+ &app,
1374
+ &artifact_path,
1375
+ &recovery_server_url,
1376
+ &recovery_recording_id,
1377
+ artifact.duration_ms,
1378
+ artifact.width,
1379
+ artifact.height,
1380
+ include_mic,
1381
+ include_system_audio,
1382
+ has_camera,
1383
+ Some(error),
1384
+ );
1385
+ }
1386
+ _ => {}
1387
+ }
1388
+ result
1305
1389
  }
1306
1390
 
1307
1391
  #[tauri::command]
@@ -1365,6 +1449,7 @@ pub(crate) async fn rewind_clip_stop_and_save(
1365
1449
  &folder_name,
1366
1450
  include_mic,
1367
1451
  include_system_audio,
1452
+ None,
1368
1453
  )?;
1369
1454
  native_screen::save_finalized_native_artifact_to_local_export(
1370
1455
  &app,
@@ -32,6 +32,12 @@ Read the relevant skill before deeper work:
32
32
  combined dashboard/report requests.
33
33
  - For form setup/configuration previews, call `preview-form`. It returns a
34
34
  native inline summary/table and an "Open editor" expansion path.
35
+ - For product usage, agent-native signup, conversion, app-wide event, or other
36
+ data-owned-by-sibling questions, use `describe-workspace-apps` when ownership
37
+ is unclear, then delegate a narrow natural-language question with `call-agent`
38
+ to the owning app. In workspaces with Analytics, it normally owns first-party
39
+ signup, conversion, and app-usage metrics. Do not invent SQL or query another
40
+ app's database.
35
41
  - For an anonymous feedback form or survey, create all fields in one
36
42
  `create-form` call with `status: "published"`, verify the persisted form, and
37
43
  copy the returned `publicUrl` verbatim. Never derive the link from `slug` or
@@ -25,6 +25,7 @@ Core rules:
25
25
  - Use the framework sharing actions for form and response resources.
26
26
  - Use \`tool-search\` with a specific query when you need a less-common capability such as sharing, extensions, resources, memory, or advanced settings. Search results load matching tool schemas into the next step.
27
27
  - If a tool fails or returns no data, say that plainly and pick the next safe action. Never fabricate success from a tool error.
28
+ - When a question is about product usage, agent-native signups, conversions, app-wide events, or data owned by another workspace app, use \`describe-workspace-apps\` if ownership is unclear, then delegate a narrow natural-language question with \`call-agent\` to the owning app. In workspaces with Analytics, it normally owns first-party signup, conversion, and app-usage metrics. Do not invent SQL or query another app's database.
28
29
 
29
30
  Form UX guidance: keep forms focused with clear labels, sensible validation, minimal required fields, and progressive disclosure for advanced settings.`;
30
31
 
@@ -26,9 +26,10 @@ Read the relevant skill before deeper work:
26
26
  is connected, synthetic `local-emails` data otherwise. Call actions the same
27
27
  way either way, and never claim fallback data or a fallback send touched the
28
28
  user's real inbox.
29
- - Never send mail unless the user explicitly asks. Draft or queue for review by
30
- default; `send-email` is `needsApproval: true`. Use `queue-email-draft` for
31
- teammate or Slack-originated send requests.
29
+ - Interactive sends require explicit user approval. Draft or queue for review by
30
+ default; automation-triggered sends remain approval-gated unless the owner
31
+ opts into Mail's "Allow automations to send emails automatically" setting.
32
+ Use `queue-email-draft` for teammate or Slack-originated send requests.
32
33
  - Resolve people with `find-contact` before drafting or sending. Never guess an
33
34
  address pattern; if it returns zero matches, tell the user.
34
35
  - Read `get-mail-settings` before drafting. Use the configured `signature`
@@ -66,7 +67,7 @@ Read the relevant skill before deeper work:
66
67
  | `get-hubspot-contact` | HubSpot contact + deals + tickets by email. |
67
68
  | `create-attachment-upload` | Short-lived upload URL for an attachment. |
68
69
  | `manage-draft` | Create/update/delete a `compose-{id}` draft. |
69
- | `send-email` | Real send; `needsApproval: true`. |
70
+ | `send-email` | Real send; interactive calls require approval, while automation calls require the Mail opt-in setting. |
70
71
  | `queue-email-draft` / `list-queued-drafts` / `update-queued-draft` / `open-queued-draft` / `send-queued-drafts` | Teammate/Slack draft review. |
71
72
  | `mark-read` / `mark-thread-read` / `star-email` / `archive-email` / `unarchive-email` / `trash-email` / `untrash-email` / `move-email` | Message/thread state; `mark-read` does bulk cleanup. |
72
73
  | `send-scheduled-email-now` / `cancel-scheduled-email` | Send or cancel a scheduled send. |
@@ -4,10 +4,14 @@ import { getUserSetting } from "@agent-native/core/settings";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { resolveAutomationModelSettings } from "../server/lib/automation-model.js";
7
+ import {
8
+ allowsAutomationSends,
9
+ type MailAutomationSettings,
10
+ } from "../server/lib/automation-settings.js";
7
11
 
8
12
  export default defineAction({
9
13
  description:
10
- "Read the engine and model used to evaluate inbox automation rules, falling back to the app's configured agent engine.",
14
+ "Read the engine, model, and email-send permission used by inbox automations, falling back to the app's configured agent engine.",
11
15
  schema: z.object({}),
12
16
  http: { method: "GET" },
13
17
  agentTool: false,
@@ -15,10 +19,17 @@ export default defineAction({
15
19
  const ownerEmail = getRequestUserEmail();
16
20
  if (!ownerEmail) throw new Error("Unauthenticated");
17
21
 
18
- const data = (await getUserSetting(ownerEmail, "automation-settings")) as {
19
- engine?: string;
20
- model?: string;
21
- } | null;
22
- return resolveAutomationModelSettings(ownerEmail, data);
22
+ const data = (await getUserSetting(
23
+ ownerEmail,
24
+ "automation-settings",
25
+ )) as MailAutomationSettings | null;
26
+ const modelSettings = await resolveAutomationModelSettings(
27
+ ownerEmail,
28
+ data,
29
+ );
30
+ return {
31
+ ...modelSettings,
32
+ allowAutomationSends: allowsAutomationSends(data),
33
+ };
23
34
  },
24
35
  });
@@ -1,4 +1,5 @@
1
1
  import { defineAction } from "@agent-native/core";
2
+ import type { ActionRunContext } from "@agent-native/core/action";
2
3
  import { writeAppState } from "@agent-native/core/application-state";
3
4
  import { emit } from "@agent-native/core/event-bus";
4
5
  import { setOAuthDisplayName } from "@agent-native/core/oauth-tokens";
@@ -8,6 +9,7 @@ import { getUserSetting } from "@agent-native/core/settings";
8
9
  import { nanoid } from "nanoid";
9
10
  import { z } from "zod";
10
11
 
12
+ import { requiresEmailSendApproval } from "../server/lib/automation-settings.js";
11
13
  import {
12
14
  collectLinks,
13
15
  newClickToken,
@@ -109,7 +111,7 @@ const attachmentSchema = z.object({
109
111
 
110
112
  export default defineAction({
111
113
  description:
112
- "Send an email via Gmail. IMPORTANT: Never call this unless the user explicitly asks to send — always draft first and show the content to the user for review before sending.",
114
+ "Send an email via Gmail. Interactive sends require approval. Automation-triggered sends require the owner to opt in through Mail settings; otherwise they remain approval-gated.",
113
115
  schema: z.object({
114
116
  to: z.string().describe("Recipient email(s), comma-separated"),
115
117
  subject: z.string().describe("Email subject"),
@@ -135,16 +137,22 @@ export default defineAction({
135
137
  "Files to attach. Each entry must reference a previously-uploaded file by its server-side `filename`. The upload must have been created via the media-upload endpoint before calling this action.",
136
138
  ),
137
139
  }),
138
- // Human-in-the-loop gate: actually sending an email is outward-facing and
139
- // hard to undo, so the agent can never send without a human approving the
140
- // specific call. The loop pauses with `approval_required`; the user approves
141
- // before the message goes out. Drafting/queueing is unaffected — only the
142
- // real send is gated. This is the canonical (and intentionally rare) use of
143
- // `needsApproval` in the framework.
144
- needsApproval: true,
145
- run: async (args) => {
140
+ // Interactive sends stay human-approved. Event-triggered automations may
141
+ // opt out through the owner's Mail Automation settings, which are read from
142
+ // trusted action context rather than from tool input.
143
+ needsApproval: (_args, ctx?: ActionRunContext) =>
144
+ requiresEmailSendApproval(ctx),
145
+ run: async (args, ctx) => {
146
146
  const ownerEmail = getRequestUserEmail();
147
147
  if (!ownerEmail) throw new Error("no authenticated user");
148
+ if (
149
+ ctx?.caller === "automation" &&
150
+ (await requiresEmailSendApproval(ctx))
151
+ ) {
152
+ throw new Error(
153
+ "Automation email sending is disabled. Enable it in Mail settings to send automatically.",
154
+ );
155
+ }
148
156
  const settings = await readSettings();
149
157
 
150
158
  // Resolve attachments eagerly — fail before touching Gmail if any are missing.
@@ -1,6 +1,8 @@
1
1
  import { defineAction } from "@agent-native/core";
2
+ import type { ActionRunContext } from "@agent-native/core/action";
2
3
  import { z } from "zod";
3
4
 
5
+ import { requiresEmailSendApproval } from "../server/lib/automation-settings.js";
4
6
  import {
5
7
  claimQueuedDraftForSending,
6
8
  listQueuedDrafts,
@@ -32,7 +34,10 @@ type SendOutcome =
32
34
  | { outcome: "skipped"; id: string; reason: string }
33
35
  | { outcome: "failed"; id: string; error: string };
34
36
 
35
- async function sendOne(id: string): Promise<SendOutcome> {
37
+ async function sendOne(
38
+ id: string,
39
+ actionContext?: ActionRunContext,
40
+ ): Promise<SendOutcome> {
36
41
  const claim = await claimQueuedDraftForSending(id);
37
42
  if (!claim.claimed) {
38
43
  if (claim.reason === "sent") {
@@ -50,14 +55,17 @@ async function sendOne(id: string): Promise<SendOutcome> {
50
55
 
51
56
  const { ctx, draft, claimId, priorStatus } = claim;
52
57
  try {
53
- const result = await (sendEmailAction as any).run({
54
- to: draft.to,
55
- cc: draft.cc || undefined,
56
- bcc: draft.bcc || undefined,
57
- subject: draft.subject,
58
- body: draft.body,
59
- account: draft.accountEmail || undefined,
60
- });
58
+ const result = await sendEmailAction.run(
59
+ {
60
+ to: draft.to,
61
+ cc: draft.cc || undefined,
62
+ bcc: draft.bcc || undefined,
63
+ subject: draft.subject,
64
+ body: draft.body,
65
+ account: draft.accountEmail || undefined,
66
+ },
67
+ actionContext,
68
+ );
61
69
 
62
70
  if (typeof result === "string" && result.startsWith("Error")) {
63
71
  throw new Error(result);
@@ -95,7 +103,9 @@ export default defineAction({
95
103
  .optional()
96
104
  .describe("Maximum drafts to send when all=true"),
97
105
  }),
98
- run: async (args) => {
106
+ needsApproval: (_args, ctx?: ActionRunContext) =>
107
+ requiresEmailSendApproval(ctx),
108
+ run: async (args, ctx) => {
99
109
  let ids: string[] = [];
100
110
 
101
111
  if (args.all) {
@@ -127,7 +137,7 @@ export default defineAction({
127
137
  const failed: Array<{ id: string; error: string }> = [];
128
138
 
129
139
  for (const id of ids) {
130
- const result = await sendOne(id);
140
+ const result = await sendOne(id, ctx);
131
141
  if (result.outcome === "sent") {
132
142
  sent.push(result);
133
143
  } else if (result.outcome === "failed") {
@@ -3,12 +3,18 @@ import { getRequestUserEmail } from "@agent-native/core/server";
3
3
  import { getUserSetting, putUserSetting } from "@agent-native/core/settings";
4
4
  import { z } from "zod";
5
5
 
6
+ import type { MailAutomationSettings } from "../server/lib/automation-settings.js";
7
+
6
8
  export default defineAction({
7
9
  description:
8
- "Set the engine and model used to evaluate inbox automation rules.",
10
+ "Set the engine and model used to evaluate inbox automation rules, and choose whether automations may send email without per-message approval.",
9
11
  schema: z.object({
10
12
  engine: z.string().optional().describe("Agent engine id"),
11
13
  model: z.string().optional().describe("Model id for that engine"),
14
+ allowAutomationSends: z
15
+ .boolean()
16
+ .optional()
17
+ .describe("Allow automation-triggered emails to send without approval"),
12
18
  }),
13
19
  http: { method: "PUT" },
14
20
  agentTool: false,
@@ -17,17 +23,25 @@ export default defineAction({
17
23
  if (!ownerEmail) throw new Error("Unauthenticated");
18
24
 
19
25
  const existing =
20
- ((await getUserSetting(ownerEmail, "automation-settings")) as {
21
- engine?: string;
22
- model?: string;
23
- } | null) || {};
26
+ ((await getUserSetting(
27
+ ownerEmail,
28
+ "automation-settings",
29
+ )) as MailAutomationSettings | null) || {};
24
30
  const updated = {
25
31
  ...existing,
26
32
  ...(args.engine ? { engine: args.engine } : {}),
27
33
  ...(args.model ? { model: args.model } : {}),
34
+ ...(args.allowAutomationSends !== undefined
35
+ ? { allowAutomationSends: args.allowAutomationSends }
36
+ : {}),
28
37
  };
29
38
  await putUserSetting(ownerEmail, "automation-settings", updated);
30
39
 
31
- return { success: true, engine: updated.engine, model: updated.model };
40
+ return {
41
+ success: true,
42
+ engine: updated.engine,
43
+ model: updated.model,
44
+ allowAutomationSends: updated.allowAutomationSends === true,
45
+ };
32
46
  },
33
47
  });
@@ -478,6 +478,11 @@ const messages = {
478
478
  lastRun: "Last run:",
479
479
  automationsDescription:
480
480
  "Rules that automatically process new inbox emails using AI.",
481
+ allowAutomationSends: "Allow automations to send emails automatically",
482
+ allowAutomationSendsDescription:
483
+ "Off by default. Turn this on when you want automations to send emails without asking for approval each time.",
484
+ automationSendSettingSaveFailed:
485
+ "Could not save the automation email setting.",
481
486
  loadingModels: "Loading models",
482
487
  newRule: "New rule",
483
488
  noAutomationRules: "No automation rules yet.",
@@ -868,6 +868,12 @@ function TriggersSubsection() {
868
868
 
869
869
  // ─── Automations Section ─────────────────────────────────────────────────────
870
870
 
871
+ type AutomationSettings = {
872
+ engine?: string;
873
+ model?: string;
874
+ allowAutomationSends: boolean;
875
+ };
876
+
871
877
  function AutomationsSection() {
872
878
  const t = useT();
873
879
  const { data: rules = [], isLoading } = useAutomations();
@@ -896,9 +902,9 @@ function AutomationsSection() {
896
902
  // Refetch on any settings write or agent action so agent-driven changes
897
903
  // (e.g. update-automation-settings) show up without a manual refresh.
898
904
  const settingsSync = useChangeVersions(["settings", "action"]);
899
- const { data: autoSettings } = useQuery({
905
+ const { data: autoSettings } = useQuery<AutomationSettings>({
900
906
  queryKey: ["automation-settings", settingsSync],
901
- queryFn: async () => {
907
+ queryFn: async (): Promise<AutomationSettings> => {
902
908
  try {
903
909
  return await callAction(
904
910
  "get-automation-settings",
@@ -906,7 +912,7 @@ function AutomationsSection() {
906
912
  { method: "GET" },
907
913
  );
908
914
  } catch {
909
- return { model: defaultModel };
915
+ return { model: defaultModel, allowAutomationSends: false };
910
916
  }
911
917
  },
912
918
  staleTime: 30_000,
@@ -914,6 +920,7 @@ function AutomationsSection() {
914
920
  });
915
921
 
916
922
  const queryClient = useQueryClient();
923
+ const [isSavingAutomationSends, setIsSavingAutomationSends] = useState(false);
917
924
  const selectedModel = autoSettings?.model || defaultModel;
918
925
  const selectedEngine =
919
926
  autoSettings?.engine ||
@@ -930,7 +937,13 @@ function AutomationsSection() {
930
937
  const handleModelChange = async (value: string) => {
931
938
  const [engine, model] = value.split("::");
932
939
  if (!engine || !model) return;
933
- queryClient.setQueryData(["automation-settings"], { engine, model });
940
+ queryClient.setQueriesData<AutomationSettings>(
941
+ { queryKey: ["automation-settings"] },
942
+ (current) =>
943
+ current
944
+ ? { ...current, engine, model }
945
+ : { engine, model, allowAutomationSends: false },
946
+ );
934
947
  await callAction(
935
948
  "update-automation-settings",
936
949
  { engine, model },
@@ -938,6 +951,40 @@ function AutomationsSection() {
938
951
  );
939
952
  };
940
953
 
954
+ const handleAutomationSendsChange = async (enabled: boolean) => {
955
+ if (!autoSettings || isSavingAutomationSends) return;
956
+ const previous = autoSettings.allowAutomationSends;
957
+ queryClient.setQueriesData<AutomationSettings>(
958
+ { queryKey: ["automation-settings"] },
959
+ (current) =>
960
+ current ? { ...current, allowAutomationSends: enabled } : current,
961
+ );
962
+ setIsSavingAutomationSends(true);
963
+ try {
964
+ await callAction(
965
+ "update-automation-settings",
966
+ { allowAutomationSends: enabled },
967
+ { method: "PUT" },
968
+ );
969
+ } catch (error) {
970
+ queryClient.setQueriesData<AutomationSettings>(
971
+ { queryKey: ["automation-settings"] },
972
+ (current) =>
973
+ current ? { ...current, allowAutomationSends: previous } : current,
974
+ );
975
+ toast.error(
976
+ error instanceof Error
977
+ ? error.message
978
+ : t("settings.automationSendSettingSaveFailed"),
979
+ );
980
+ } finally {
981
+ setIsSavingAutomationSends(false);
982
+ await queryClient.invalidateQueries({
983
+ queryKey: ["automation-settings"],
984
+ });
985
+ }
986
+ };
987
+
941
988
  const handleCreate = (data: {
942
989
  name: string;
943
990
  condition: string;
@@ -996,6 +1043,20 @@ function AutomationsSection() {
996
1043
  </Button>
997
1044
  </div>
998
1045
 
1046
+ <div className="max-w-2xl mb-6">
1047
+ {autoSettings ? (
1048
+ <SettingsSwitchRow
1049
+ title={t("settings.allowAutomationSends")}
1050
+ description={t("settings.allowAutomationSendsDescription")}
1051
+ checked={autoSettings.allowAutomationSends}
1052
+ disabled={isSavingAutomationSends}
1053
+ onCheckedChange={handleAutomationSendsChange}
1054
+ />
1055
+ ) : (
1056
+ <Skeleton className="h-16 w-full" />
1057
+ )}
1058
+ </div>
1059
+
999
1060
  {/* Content */}
1000
1061
  <div className="max-w-2xl space-y-2">
1001
1062
  {/* New rule form */}
@@ -1219,7 +1280,7 @@ function DraftingSection() {
1219
1280
  );
1220
1281
  }
1221
1282
 
1222
- function TrackingRow({
1283
+ function SettingsSwitchRow({
1223
1284
  title,
1224
1285
  description,
1225
1286
  checked,
@@ -1281,13 +1342,13 @@ function TrackingSection() {
1281
1342
  </>
1282
1343
  ) : (
1283
1344
  <>
1284
- <TrackingRow
1345
+ <SettingsSwitchRow
1285
1346
  title={t("settings.trackEmailOpens")}
1286
1347
  description={t("settings.trackEmailOpensDescription")}
1287
1348
  checked={tracking.opens}
1288
1349
  onCheckedChange={(v) => update({ opens: v })}
1289
1350
  />
1290
- <TrackingRow
1351
+ <SettingsSwitchRow
1291
1352
  title={t("settings.trackLinkClicks")}
1292
1353
  description={t("settings.trackLinkClicksDescription")}
1293
1354
  checked={tracking.clicks}
@@ -0,0 +1,27 @@
1
+ import type { ActionRunContext } from "@agent-native/core/action";
2
+ import { getUserSetting } from "@agent-native/core/settings";
3
+
4
+ export type MailAutomationSettings = {
5
+ engine?: string;
6
+ model?: string;
7
+ allowAutomationSends?: boolean;
8
+ };
9
+
10
+ export function allowsAutomationSends(settings: unknown): boolean {
11
+ return (
12
+ typeof settings === "object" &&
13
+ settings !== null &&
14
+ "allowAutomationSends" in settings &&
15
+ (settings as { allowAutomationSends?: unknown }).allowAutomationSends ===
16
+ true
17
+ );
18
+ }
19
+
20
+ export async function requiresEmailSendApproval(
21
+ ctx?: ActionRunContext,
22
+ ): Promise<boolean> {
23
+ if (ctx?.caller !== "automation" || !ctx.userEmail) return true;
24
+
25
+ const settings = await getUserSetting(ctx.userEmail, "automation-settings");
26
+ return !allowsAutomationSends(settings);
27
+ }
@@ -151,6 +151,8 @@ Be concise and helpful. When summarizing emails, include sender, subject, and a
151
151
  You can create and manage email automation rules that process new inbox emails automatically using AI.
152
152
  Use manage-automations to create rules like "auto-label newsletters", "star emails from my boss", etc.
153
153
 
154
+ Sending email from an automation is opt-in. Mail keeps "Allow automations to send emails automatically" off by default. When it is off, an automation may draft or queue an email, but a real send remains approval-gated. Turning it on lets event-triggered automations send without asking for approval each time; it does not remove approval from normal interactive sends.
155
+
154
156
  Examples:
155
157
  - User says "auto-label newsletters" \u2192 create rule with condition "from a newsletter or marketing mailing list" and action label:"newsletters"
156
158
  - User says "archive marketing emails" \u2192 create rule with condition "marketing or promotional email" and action archive
@@ -441,9 +441,9 @@ export default function EditorSidebar({
441
441
  const nextSlideId = slides[nextIndex].id;
442
442
  onSelectSlide(nextSlideId);
443
443
  requestAnimationFrame(() => {
444
- slideButtonRefs.current.get(nextSlideId)?.focus({
445
- preventScroll: true,
446
- });
444
+ const nextButton = slideButtonRefs.current.get(nextSlideId);
445
+ nextButton?.focus({ preventScroll: true });
446
+ nextButton?.scrollIntoView({ block: "nearest" });
447
447
  });
448
448
  }
449
449
  };