@agent-native/core 0.153.2 → 0.153.5

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 (92) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/AGENTS.md +4 -0
  3. package/corpus/templates/analytics/actions/account-deep-dive.ts +1 -0
  4. package/corpus/templates/analytics/actions/bigquery-query.ts +1 -0
  5. package/corpus/templates/analytics/actions/bigquery.ts +1 -0
  6. package/corpus/templates/analytics/actions/content-calendar-schema.ts +1 -0
  7. package/corpus/templates/analytics/actions/content-calendar.ts +1 -0
  8. package/corpus/templates/analytics/actions/gcloud.ts +1 -0
  9. package/corpus/templates/analytics/actions/get-data-program.ts +1 -0
  10. package/corpus/templates/analytics/actions/get-error-issue.ts +1 -0
  11. package/corpus/templates/analytics/actions/get-monitor-stats.ts +1 -0
  12. package/corpus/templates/analytics/actions/get-monitor.ts +1 -0
  13. package/corpus/templates/analytics/actions/get-session-replay-events.ts +1 -0
  14. package/corpus/templates/analytics/actions/get-session-replay-summary.ts +1 -0
  15. package/corpus/templates/analytics/actions/get-session-replay-timeline.ts +1 -0
  16. package/corpus/templates/analytics/actions/gong-calls.ts +1 -0
  17. package/corpus/templates/analytics/actions/gong-native-insights.ts +1 -0
  18. package/corpus/templates/analytics/actions/grafana.ts +1 -0
  19. package/corpus/templates/analytics/actions/hubspot-deals.ts +1 -0
  20. package/corpus/templates/analytics/actions/hubspot-metrics.ts +1 -0
  21. package/corpus/templates/analytics/actions/hubspot-pipelines.ts +1 -0
  22. package/corpus/templates/analytics/actions/hubspot-records.ts +1 -0
  23. package/corpus/templates/analytics/actions/jira-search.ts +1 -0
  24. package/corpus/templates/analytics/actions/jira.ts +1 -0
  25. package/corpus/templates/analytics/actions/list-error-issues.ts +1 -0
  26. package/corpus/templates/analytics/actions/list-monitors.ts +1 -0
  27. package/corpus/templates/analytics/actions/list-session-recordings.ts +1 -0
  28. package/corpus/templates/analytics/actions/match-error-issues.ts +1 -0
  29. package/corpus/templates/analytics/actions/prometheus.ts +1 -0
  30. package/corpus/templates/analytics/actions/pylon-issues.ts +1 -0
  31. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -0
  32. package/corpus/templates/analytics/actions/query-dashboard-panel.ts +1 -0
  33. package/corpus/templates/analytics/actions/query-inbound-forms.ts +1 -0
  34. package/corpus/templates/analytics/actions/run-monitor-check.ts +1 -0
  35. package/corpus/templates/analytics/actions/sentry.ts +1 -0
  36. package/corpus/templates/analytics/actions/seo-blog-pages.ts +1 -0
  37. package/corpus/templates/analytics/actions/seo-page-keywords.ts +1 -0
  38. package/corpus/templates/analytics/actions/seo-top-keywords.ts +1 -0
  39. package/corpus/templates/analytics/actions/slack-messages.ts +1 -0
  40. package/corpus/templates/analytics/actions/stripe.ts +1 -0
  41. package/corpus/templates/analytics/actions/test-custom-api-connection.ts +1 -0
  42. package/corpus/templates/analytics/server/lib/real-data-actions.ts +38 -47
  43. package/corpus/templates/analytics/server/plugins/agent-chat.ts +41 -34
  44. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +9 -6
  45. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +70 -10
  46. package/corpus/templates/forms/AGENTS.md +6 -0
  47. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -0
  48. package/corpus/templates/mail/AGENTS.md +5 -4
  49. package/corpus/templates/mail/actions/get-automation-settings.ts +17 -6
  50. package/corpus/templates/mail/actions/send-email.ts +17 -9
  51. package/corpus/templates/mail/actions/send-queued-drafts.ts +21 -11
  52. package/corpus/templates/mail/actions/update-automation-settings.ts +20 -6
  53. package/corpus/templates/mail/app/i18n/en-US.ts +5 -0
  54. package/corpus/templates/mail/app/pages/SettingsPage.tsx +68 -7
  55. package/corpus/templates/mail/server/lib/automation-settings.ts +27 -0
  56. package/corpus/templates/mail/server/plugins/agent-chat.ts +2 -0
  57. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +3 -3
  58. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +17 -4
  59. package/corpus/templates/slides/app/context/DeckContext.tsx +3 -2
  60. package/corpus/templates/slides/app/pages/DeckEditor.tsx +44 -0
  61. package/corpus/templates/slides/changelog/2026-08-12-arrow-key-navigation-in-the-slide-film-strip-now-scrolls-sli.md +6 -0
  62. package/corpus/templates/slides/changelog/2026-08-12-duplicate-a-slide-from-the-film-strip-with-cmd-ctrl-c-then-c.md +6 -0
  63. package/corpus/templates/slides/changelog/2026-08-12-presenting-a-deck-now-returns-you-to-the-slide-you-were-view.md +6 -0
  64. package/dist/action.d.ts +13 -0
  65. package/dist/action.js +3 -0
  66. package/dist/agent/engine/tool-call-journal-seed.d.ts +27 -10
  67. package/dist/agent/engine/tool-call-journal-seed.js +46 -32
  68. package/dist/agent/production-agent.d.ts +33 -0
  69. package/dist/agent/production-agent.js +172 -57
  70. package/dist/agent/run-loop-with-resume.d.ts +1 -1
  71. package/dist/agent/run-loop-with-resume.js +49 -35
  72. package/dist/client/agent-chat-adapter.js +204 -273
  73. package/dist/client/resources/mcp-integration-catalog.d.ts +7 -0
  74. package/dist/client/resources/mcp-integration-catalog.js +14 -1
  75. package/dist/client/settings/AgentsSection.js +46 -30
  76. package/dist/client/sse-event-processor.d.ts +7 -0
  77. package/dist/client/sse-event-processor.js +21 -1
  78. package/dist/collab/awareness.d.ts +2 -2
  79. package/dist/collab/routes.d.ts +1 -1
  80. package/dist/collab/struct-routes.d.ts +1 -1
  81. package/dist/notifications/routes.d.ts +1 -1
  82. package/dist/observability/routes.d.ts +3 -3
  83. package/dist/provider-api/actions/github-repo-files.js +1 -0
  84. package/dist/provider-api/actions/provider-api.js +1 -0
  85. package/dist/provider-api/actions/staged-datasets.js +1 -0
  86. package/dist/provider-api/corpus-jobs.js +1 -0
  87. package/dist/server/action-discovery.js +2 -0
  88. package/dist/server/agent-chat-plugin.js +1 -0
  89. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  90. package/dist/server/realtime-token.d.ts +1 -1
  91. package/dist/server/transcribe-voice.d.ts +1 -1
  92. package/package.json +3 -3
@@ -4486,8 +4486,8 @@ pub(crate) fn persist_shared_clip_recording(
4486
4486
  server_url: &str,
4487
4487
  recording_id: &str,
4488
4488
  duration_ms: u128,
4489
- width: u32,
4490
- height: u32,
4489
+ width: Option<u32>,
4490
+ height: Option<u32>,
4491
4491
  include_mic: bool,
4492
4492
  include_system_audio: bool,
4493
4493
  has_camera: bool,
@@ -4506,8 +4506,8 @@ pub(crate) fn persist_shared_clip_recording(
4506
4506
  segment_paths: Vec::new(),
4507
4507
  mime_type: MP4_RECORDING_MIME_TYPE.to_string(),
4508
4508
  duration_ms,
4509
- width: Some(width),
4510
- height: Some(height),
4509
+ width,
4510
+ height,
4511
4511
  bytes,
4512
4512
  has_audio: include_mic || include_system_audio,
4513
4513
  mic_captured: include_mic,
@@ -4532,8 +4532,11 @@ pub(crate) fn clear_shared_clip_recording(app: &AppHandle, recording_id: &str, p
4532
4532
  let saved = read_saved_recording_metadata(app, recording_id).ok();
4533
4533
  if let Some(saved) = saved {
4534
4534
  clear_saved_recording_after_success(app, &saved);
4535
- } else if let Err(error) = remove_saved_file(path, "shared Clip temporary artifact") {
4536
- eprintln!("[clips-tray] shared Clip cleanup failed: {error}");
4535
+ } else {
4536
+ remove_recording_intent(path);
4537
+ if let Err(error) = remove_saved_file(path, "shared Clip temporary artifact") {
4538
+ eprintln!("[clips-tray] shared Clip cleanup failed: {error}");
4539
+ }
4537
4540
  }
4538
4541
  let _ = app.emit("clips:pending-uploads-changed", ());
4539
4542
  }
@@ -1038,6 +1038,7 @@ fn materialize(
1038
1038
  label: &str,
1039
1039
  include_mic: bool,
1040
1040
  include_system_audio: bool,
1041
+ recovery: Option<(&str, &str, bool)>,
1041
1042
  ) -> Result<FinalizedNativeArtifact, String> {
1042
1043
  let active = take_active(state)?;
1043
1044
  let mut intervals = active.intervals.clone();
@@ -1112,10 +1113,25 @@ fn materialize(
1112
1113
  }
1113
1114
  let output = artifact_path(app, label)?;
1114
1115
  let audio = select_audio(&active.sources, include_mic, include_system_audio)?;
1115
- native_screen::materialize_mp4_slices_exact(&slices, &output, audio)?;
1116
1116
  let first = first_segment
1117
1117
  .as_ref()
1118
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)?;
1119
1135
  Ok(FinalizedNativeArtifact::rewind_mp4(
1120
1136
  output,
1121
1137
  duration_ms,
@@ -1199,8 +1215,8 @@ pub(crate) async fn rewind_clip_stop_and_upload(
1199
1215
  &server_url,
1200
1216
  &recording_id,
1201
1217
  logical_duration_ms as u128,
1202
- sink_width,
1203
- sink_height,
1218
+ Some(sink_width),
1219
+ Some(sink_height),
1204
1220
  include_mic,
1205
1221
  include_system_audio,
1206
1222
  has_camera,
@@ -1230,8 +1246,8 @@ pub(crate) async fn rewind_clip_stop_and_upload(
1230
1246
  &server_url,
1231
1247
  &recording_id,
1232
1248
  result.duration_ms as u128,
1233
- result.width,
1234
- result.height,
1249
+ Some(result.width),
1250
+ Some(result.height),
1235
1251
  include_mic,
1236
1252
  include_system_audio,
1237
1253
  has_camera,
@@ -1257,8 +1273,8 @@ pub(crate) async fn rewind_clip_stop_and_upload(
1257
1273
  &server_url,
1258
1274
  &recording_id,
1259
1275
  result.duration_ms as u128,
1260
- result.width,
1261
- result.height,
1276
+ Some(result.width),
1277
+ Some(result.height),
1262
1278
  include_mic,
1263
1279
  include_system_audio,
1264
1280
  has_camera,
@@ -1314,19 +1330,62 @@ pub(crate) async fn rewind_clip_stop_and_upload(
1314
1330
  &recording_id,
1315
1331
  include_mic,
1316
1332
  include_system_audio,
1333
+ Some((&server_url, &recording_id, has_camera)),
1334
+ )?;
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,
1317
1347
  )?;
1318
- native_screen::upload_finalized_native_artifact(
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(
1319
1352
  &app,
1320
1353
  &artifact,
1321
1354
  server_url,
1322
- recording_id,
1355
+ recording_id.clone(),
1323
1356
  auth_token.unwrap_or_default(),
1324
1357
  cookie.unwrap_or_default(),
1325
1358
  NativeUploadMode::from_option(upload_mode),
1326
1359
  include_mic || include_system_audio,
1327
1360
  has_camera,
1328
1361
  )
1329
- .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
1330
1389
  }
1331
1390
 
1332
1391
  #[tauri::command]
@@ -1390,6 +1449,7 @@ pub(crate) async fn rewind_clip_stop_and_save(
1390
1449
  &folder_name,
1391
1450
  include_mic,
1392
1451
  include_system_audio,
1452
+ None,
1393
1453
  )?;
1394
1454
  native_screen::save_finalized_native_artifact_to_local_export(
1395
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
  };