@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/AGENTS.md +4 -0
- package/corpus/templates/analytics/actions/account-deep-dive.ts +1 -0
- package/corpus/templates/analytics/actions/bigquery-query.ts +1 -0
- package/corpus/templates/analytics/actions/bigquery.ts +1 -0
- package/corpus/templates/analytics/actions/content-calendar-schema.ts +1 -0
- package/corpus/templates/analytics/actions/content-calendar.ts +1 -0
- package/corpus/templates/analytics/actions/gcloud.ts +1 -0
- package/corpus/templates/analytics/actions/get-data-program.ts +1 -0
- package/corpus/templates/analytics/actions/get-error-issue.ts +1 -0
- package/corpus/templates/analytics/actions/get-monitor-stats.ts +1 -0
- package/corpus/templates/analytics/actions/get-monitor.ts +1 -0
- package/corpus/templates/analytics/actions/get-session-replay-events.ts +1 -0
- package/corpus/templates/analytics/actions/get-session-replay-summary.ts +1 -0
- package/corpus/templates/analytics/actions/get-session-replay-timeline.ts +1 -0
- package/corpus/templates/analytics/actions/gong-calls.ts +1 -0
- package/corpus/templates/analytics/actions/gong-native-insights.ts +1 -0
- package/corpus/templates/analytics/actions/grafana.ts +1 -0
- package/corpus/templates/analytics/actions/hubspot-deals.ts +1 -0
- package/corpus/templates/analytics/actions/hubspot-metrics.ts +1 -0
- package/corpus/templates/analytics/actions/hubspot-pipelines.ts +1 -0
- package/corpus/templates/analytics/actions/hubspot-records.ts +1 -0
- package/corpus/templates/analytics/actions/jira-search.ts +1 -0
- package/corpus/templates/analytics/actions/jira.ts +1 -0
- package/corpus/templates/analytics/actions/list-error-issues.ts +1 -0
- package/corpus/templates/analytics/actions/list-monitors.ts +1 -0
- package/corpus/templates/analytics/actions/list-session-recordings.ts +1 -0
- package/corpus/templates/analytics/actions/match-error-issues.ts +1 -0
- package/corpus/templates/analytics/actions/prometheus.ts +1 -0
- package/corpus/templates/analytics/actions/pylon-issues.ts +1 -0
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -0
- package/corpus/templates/analytics/actions/query-dashboard-panel.ts +1 -0
- package/corpus/templates/analytics/actions/query-inbound-forms.ts +1 -0
- package/corpus/templates/analytics/actions/run-monitor-check.ts +1 -0
- package/corpus/templates/analytics/actions/sentry.ts +1 -0
- package/corpus/templates/analytics/actions/seo-blog-pages.ts +1 -0
- package/corpus/templates/analytics/actions/seo-page-keywords.ts +1 -0
- package/corpus/templates/analytics/actions/seo-top-keywords.ts +1 -0
- package/corpus/templates/analytics/actions/slack-messages.ts +1 -0
- package/corpus/templates/analytics/actions/stripe.ts +1 -0
- package/corpus/templates/analytics/actions/test-custom-api-connection.ts +1 -0
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +38 -47
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +41 -34
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +9 -6
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +70 -10
- package/corpus/templates/forms/AGENTS.md +6 -0
- package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/mail/AGENTS.md +5 -4
- package/corpus/templates/mail/actions/get-automation-settings.ts +17 -6
- package/corpus/templates/mail/actions/send-email.ts +17 -9
- package/corpus/templates/mail/actions/send-queued-drafts.ts +21 -11
- package/corpus/templates/mail/actions/update-automation-settings.ts +20 -6
- package/corpus/templates/mail/app/i18n/en-US.ts +5 -0
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +68 -7
- package/corpus/templates/mail/server/lib/automation-settings.ts +27 -0
- package/corpus/templates/mail/server/plugins/agent-chat.ts +2 -0
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +3 -3
- package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +17 -4
- package/corpus/templates/slides/app/context/DeckContext.tsx +3 -2
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +44 -0
- package/corpus/templates/slides/changelog/2026-08-12-arrow-key-navigation-in-the-slide-film-strip-now-scrolls-sli.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-12-duplicate-a-slide-from-the-film-strip-with-cmd-ctrl-c-then-c.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-12-presenting-a-deck-now-returns-you-to-the-slide-you-were-view.md +6 -0
- package/dist/action.d.ts +13 -0
- package/dist/action.js +3 -0
- package/dist/agent/engine/tool-call-journal-seed.d.ts +27 -10
- package/dist/agent/engine/tool-call-journal-seed.js +46 -32
- package/dist/agent/production-agent.d.ts +33 -0
- package/dist/agent/production-agent.js +172 -57
- package/dist/agent/run-loop-with-resume.d.ts +1 -1
- package/dist/agent/run-loop-with-resume.js +49 -35
- package/dist/client/agent-chat-adapter.js +204 -273
- package/dist/client/resources/mcp-integration-catalog.d.ts +7 -0
- package/dist/client/resources/mcp-integration-catalog.js +14 -1
- package/dist/client/settings/AgentsSection.js +46 -30
- package/dist/client/sse-event-processor.d.ts +7 -0
- package/dist/client/sse-event-processor.js +21 -1
- 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/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/provider-api/actions/github-repo-files.js +1 -0
- package/dist/provider-api/actions/provider-api.js +1 -0
- package/dist/provider-api/actions/staged-datasets.js +1 -0
- package/dist/provider-api/corpus-jobs.js +1 -0
- package/dist/server/action-discovery.js +2 -0
- package/dist/server/agent-chat-plugin.js +1 -0
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- 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
|
|
4510
|
-
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
|
|
4536
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
30
|
-
default;
|
|
31
|
-
|
|
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;
|
|
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
|
|
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(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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.
|
|
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
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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(
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 {
|
|
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.
|
|
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
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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)
|
|
445
|
-
|
|
446
|
-
});
|
|
444
|
+
const nextButton = slideButtonRefs.current.get(nextSlideId);
|
|
445
|
+
nextButton?.focus({ preventScroll: true });
|
|
446
|
+
nextButton?.scrollIntoView({ block: "nearest" });
|
|
447
447
|
});
|
|
448
448
|
}
|
|
449
449
|
};
|