@agent-native/core 0.153.2 → 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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/AGENTS.md +4 -0
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -0
- 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/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/collab/routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat-plugin.js +1 -0
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -48,6 +48,10 @@ Read the relevant skill before deeper work:
|
|
|
48
48
|
sibling-invocable because this app owns schema, source selection, and tools.
|
|
49
49
|
Prefer natural-language delegation; shaped reads are stable contracts, not
|
|
50
50
|
delegation workarounds.
|
|
51
|
+
- Analytics owns first-party product usage, app/template events, agent-native
|
|
52
|
+
signups, conversions, and other curated product metrics. Answer sibling-app
|
|
53
|
+
delegations with the built-in source and query catalog; sibling agents should
|
|
54
|
+
send a natural-language question, never SQL.
|
|
51
55
|
- For open-ended delegated requests, choose a safe default and label partial.
|
|
52
56
|
- Data integrity first. Never invent numbers, dimensions, filters, or source
|
|
53
57
|
semantics; only present values you actually retrieved, and state uncertainty.
|
|
@@ -105,6 +105,7 @@ export const ANALYTICS_OBSERVABILITY_INCIDENT_GUIDANCE =
|
|
|
105
105
|
"OBSERVABILITY INCIDENT WORKFLOW — For a named user's session or error question, resolve the user's email from context, then use list-session-recordings with userId over a bounded recent window to discover the relevant sessions. Do not require hasErrors=true for this initial lookup: replay/network/stuck-run evidence can exist while the recording's JavaScript errorCount is zero. Use hasErrors=true only when the user specifically asks for recordings with captured JavaScript errors or the recording metadata confirms that filter is appropriate. Use list-error-issues with userId or sessionRecordingId to identify a grouped issue, then get-error-issue for stack, breadcrumbs, occurrences, and linked recordings. For console diagnostics or failed network requests, create-session-replay-agent-link first and use its scoped diagnostics endpoint for detailed error text, stacks, request metadata, and bounded 5xx snippets; enumerate with kind/limit and fromMs/toMs or offset when needed. Use get-session-replay-summary and get-session-replay-timeline for the page-navigation and click sequence, and use get-session-replay-events only for additional bounded replay-event details. If no grouped error exists, correlate first-party observability events such as agent_chat_stuck_detected with query-agent-native-analytics. This and other read-only investigation tools remain available in Plan mode; run the query instead of deferring it to execution mode. Prefer these first-party actions over generic SQL. Report the matching evidence and do not claim a root cause without a corroborating error, event, or replay signal. ";
|
|
106
106
|
|
|
107
107
|
export const ANALYTICS_CROSS_APP_ROUTING_GUIDANCE =
|
|
108
|
+
"WORKSPACE APP ROUTING — Analytics is the sibling app for first-party product usage, app/template events, agent-native signups, conversions, and other curated product metrics. When another app delegates one of these questions with `call-agent`, answer it here using the built-in first-party source and query catalog; do not send the user back to another app or ask the caller to invent SQL. " +
|
|
108
109
|
"WORKSPACE APP ROUTING — Brain is the sibling app that owns company knowledge and indexed Slack context. Brain is not an Analytics extension and will not appear in `list-extensions`. When the user asks about company knowledge, decisions, meeting context, or Slack messages/context such as a named channel or thread, use `call-agent` with agent `brain` and a narrow natural-language question. Use `describe-workspace-apps` only when you need to confirm the sibling capability. Do not use `list-extensions` to find Brain, and do not use `provider-api-request` to call Brain. If Brain reports an access or source error, preserve that exact error; do not infer that the Slack bot is absent from a channel or tell the user to re-invite it. Stay in Analytics for metrics and aggregates over a named provider. ";
|
|
109
110
|
|
|
110
111
|
export const NON_ANALYTICS_REQUEST_GUIDANCE =
|
|
@@ -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
|
};
|
|
@@ -239,6 +239,17 @@ export default function PresentationView({
|
|
|
239
239
|
|
|
240
240
|
const isShared = deckId.startsWith("__shared__/");
|
|
241
241
|
|
|
242
|
+
// Exit handlers read these instead of closing over `currentIndex`/`deckId`/
|
|
243
|
+
// `isShared` so the mount-only fullscreenchange listener still lands on the
|
|
244
|
+
// right deck and slide even if this component is reused for a different
|
|
245
|
+
// deck without remounting (e.g. an agent-driven navigation).
|
|
246
|
+
const currentIndexRef = useRef(currentIndex);
|
|
247
|
+
currentIndexRef.current = currentIndex;
|
|
248
|
+
const deckIdRef = useRef(deckId);
|
|
249
|
+
deckIdRef.current = deckId;
|
|
250
|
+
const isSharedRef = useRef(isShared);
|
|
251
|
+
isSharedRef.current = isShared;
|
|
252
|
+
|
|
242
253
|
useEffect(() => {
|
|
243
254
|
setCurrentIndex((prev) => clampIndex(prev));
|
|
244
255
|
setPrevIndex((prev) =>
|
|
@@ -348,7 +359,7 @@ export default function PresentationView({
|
|
|
348
359
|
const token = deckId.replace("__shared__/", "");
|
|
349
360
|
navigate(`/share/${token}`);
|
|
350
361
|
} else {
|
|
351
|
-
navigate(`/deck/${deckId}`);
|
|
362
|
+
navigate(`/deck/${deckId}?slide=${currentIndexRef.current + 1}`);
|
|
352
363
|
}
|
|
353
364
|
}, [navigate, deckId, isShared]);
|
|
354
365
|
|
|
@@ -465,11 +476,13 @@ export default function PresentationView({
|
|
|
465
476
|
// fullscreen first — otherwise the gesture-fallback overlay handles it.
|
|
466
477
|
if (enteredFullscreenRef.current && !document.fullscreenElement) {
|
|
467
478
|
enteredFullscreenRef.current = false;
|
|
468
|
-
if (
|
|
469
|
-
const token =
|
|
479
|
+
if (isSharedRef.current) {
|
|
480
|
+
const token = deckIdRef.current.replace("__shared__/", "");
|
|
470
481
|
navigate(`/share/${token}`);
|
|
471
482
|
} else {
|
|
472
|
-
navigate(
|
|
483
|
+
navigate(
|
|
484
|
+
`/deck/${deckIdRef.current}?slide=${currentIndexRef.current + 1}`,
|
|
485
|
+
);
|
|
473
486
|
}
|
|
474
487
|
}
|
|
475
488
|
};
|
|
@@ -223,7 +223,7 @@ interface DeckContextType {
|
|
|
223
223
|
options?: UpdateSlideOptions,
|
|
224
224
|
) => void;
|
|
225
225
|
deleteSlide: (deckId: string, slideId: string) => void;
|
|
226
|
-
duplicateSlide: (deckId: string, slideId: string) =>
|
|
226
|
+
duplicateSlide: (deckId: string, slideId: string) => string | undefined;
|
|
227
227
|
reorderSlides: (deckId: string, oldIndex: number, newIndex: number) => void;
|
|
228
228
|
setDeckSlides: (deckId: string, slides: Slide[]) => void;
|
|
229
229
|
/**
|
|
@@ -2223,7 +2223,7 @@ export function DeckProvider({ children }: { children: ReactNode }) {
|
|
|
2223
2223
|
(deckId: string, slideId: string) => {
|
|
2224
2224
|
const before = decksRef.current.find((d) => d.id === deckId);
|
|
2225
2225
|
const original = before?.slides.find((slide) => slide.id === slideId);
|
|
2226
|
-
if (!before || !original) return;
|
|
2226
|
+
if (!before || !original) return undefined;
|
|
2227
2227
|
|
|
2228
2228
|
markDeckDirty(deckId);
|
|
2229
2229
|
const copiedSlide: Slide = { ...original, id: nanoid(8) };
|
|
@@ -2254,6 +2254,7 @@ export function DeckProvider({ children }: { children: ReactNode }) {
|
|
|
2254
2254
|
};
|
|
2255
2255
|
enqueueDeckOp(deckId, op);
|
|
2256
2256
|
recordUndo(before, op, { label: "Duplicate slide" });
|
|
2257
|
+
return copiedSlide.id;
|
|
2257
2258
|
},
|
|
2258
2259
|
[markDeckDirty, recordUndo, setDecksLocal],
|
|
2259
2260
|
);
|
|
@@ -807,6 +807,50 @@ export default function DeckEditor() {
|
|
|
807
807
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
808
808
|
}, [deck, id, activeSlideId, deleteSlideWithUndo, pinMode, drawMode]);
|
|
809
809
|
|
|
810
|
+
// Cmd/Ctrl+C then Cmd/Ctrl+V duplicates a slide thumbnail in the film
|
|
811
|
+
// strip. Scoped to an actually-focused thumbnail (rather than "no other
|
|
812
|
+
// handler claimed it", like the Delete-key handler below) so the shortcut
|
|
813
|
+
// can't fire from an unrelated toolbar button, dialog, or menu.
|
|
814
|
+
const copiedSlideIdRef = useRef<string | null>(null);
|
|
815
|
+
useEffect(() => {
|
|
816
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
817
|
+
if (!deck || !id) return;
|
|
818
|
+
if (!(e.metaKey || e.ctrlKey) || e.altKey) return;
|
|
819
|
+
const key = e.key.toLowerCase();
|
|
820
|
+
if (key !== "c" && key !== "v") return;
|
|
821
|
+
if (pinMode || drawMode) return;
|
|
822
|
+
if (
|
|
823
|
+
document.querySelector(
|
|
824
|
+
"[role='dialog'], [role='menu'], [role='listbox']",
|
|
825
|
+
)
|
|
826
|
+
) {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (!canEdit) return;
|
|
830
|
+
const focusedThumbnail = document.activeElement?.closest(
|
|
831
|
+
"[data-slide-thumbnail-id]",
|
|
832
|
+
);
|
|
833
|
+
if (!focusedThumbnail) return;
|
|
834
|
+
const focusedSlideId = focusedThumbnail.getAttribute(
|
|
835
|
+
"data-slide-thumbnail-id",
|
|
836
|
+
);
|
|
837
|
+
if (!focusedSlideId) return;
|
|
838
|
+
|
|
839
|
+
if (key === "c") {
|
|
840
|
+
copiedSlideIdRef.current = focusedSlideId;
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const copiedSlideId = copiedSlideIdRef.current;
|
|
844
|
+
if (!copiedSlideId) return;
|
|
845
|
+
if (!deck.slides.some((s) => s.id === copiedSlideId)) return;
|
|
846
|
+
e.preventDefault();
|
|
847
|
+
const newSlideId = duplicateSlide(id, copiedSlideId);
|
|
848
|
+
if (newSlideId) setActiveSlideId(newSlideId);
|
|
849
|
+
};
|
|
850
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
851
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
852
|
+
}, [deck, id, duplicateSlide, canEdit, pinMode, drawMode]);
|
|
853
|
+
|
|
810
854
|
// Resolve the active slide from URL/deck state. Imports replace slide IDs, so
|
|
811
855
|
// keep this valid after deck contents change instead of only on first load.
|
|
812
856
|
// Track the last URL ?slide param we processed so we can tell "the URL changed
|
|
@@ -39,6 +39,13 @@ export interface DefaultMcpIntegration {
|
|
|
39
39
|
brandAliases?: string[];
|
|
40
40
|
aliases?: string[];
|
|
41
41
|
keywords: string[];
|
|
42
|
+
/**
|
|
43
|
+
* Overrides `name` for prose intent matching in `findMcpIntegrationForText`
|
|
44
|
+
* when the display name is a common English word (e.g. "Box") that would
|
|
45
|
+
* otherwise false-positive on unrelated text. Leave unset unless the name
|
|
46
|
+
* itself is the ambiguous term; `brandAliases` still apply.
|
|
47
|
+
*/
|
|
48
|
+
promptAliases?: string[];
|
|
42
49
|
}
|
|
43
50
|
export interface McpIntegrationFormDefaults {
|
|
44
51
|
name: string;
|
|
@@ -675,6 +675,16 @@ export const DEFAULT_MCP_INTEGRATIONS = [
|
|
|
675
675
|
docsUrl: "https://developer.box.com/guides/box-mcp",
|
|
676
676
|
setupNoteKey: "mcpIntegrations.catalog.box.setupNote",
|
|
677
677
|
keywords: ["files", "folders", "documents", "enterprise content"],
|
|
678
|
+
// "Box" alone collides with everyday nouns (text box, checkbox, bounding
|
|
679
|
+
// box), so require a qualified phrase before suggesting the connection.
|
|
680
|
+
promptAliases: [
|
|
681
|
+
"Box.com",
|
|
682
|
+
"Box file",
|
|
683
|
+
"Box files",
|
|
684
|
+
"Box folder",
|
|
685
|
+
"Box folders",
|
|
686
|
+
"Box drive",
|
|
687
|
+
],
|
|
678
688
|
},
|
|
679
689
|
{
|
|
680
690
|
id: "builder-cms",
|
|
@@ -947,7 +957,10 @@ export function findMcpIntegrationForText(text, integrations = getDefaultMcpInte
|
|
|
947
957
|
isMcpConnectionFailureText(normalizedText);
|
|
948
958
|
if (!hasResourceIntent)
|
|
949
959
|
return null;
|
|
950
|
-
const matchesCanonicalName = (integration) => [
|
|
960
|
+
const matchesCanonicalName = (integration) => [
|
|
961
|
+
...(integration.promptAliases ?? [integration.name]),
|
|
962
|
+
...(integration.brandAliases ?? []),
|
|
963
|
+
].some((alias) => textContainsTerm(normalizedText, alias));
|
|
951
964
|
const canonicalMatch = integrations.find(matchesCanonicalName);
|
|
952
965
|
if (canonicalMatch)
|
|
953
966
|
return canonicalMatch;
|
|
@@ -165,12 +165,20 @@ function AgentAddPopover({ initialName = "", initialUrl = "", initialDescription
|
|
|
165
165
|
if (!trimmedName || !trimmedUrl)
|
|
166
166
|
return;
|
|
167
167
|
const trimmedDescription = description.trim();
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
168
|
+
try {
|
|
169
|
+
const ok = await onAdd(trimmedName, trimmedUrl, trimmedDescription);
|
|
170
|
+
if (ok) {
|
|
171
|
+
setAdded({
|
|
172
|
+
name: trimmedName,
|
|
173
|
+
url: trimmedUrl,
|
|
174
|
+
description: trimmedDescription,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
setCheck({
|
|
180
|
+
status: "error",
|
|
181
|
+
message: error instanceof Error ? error.message : "Could not add agent",
|
|
174
182
|
});
|
|
175
183
|
}
|
|
176
184
|
};
|
|
@@ -236,8 +244,12 @@ export function AgentsSection() {
|
|
|
236
244
|
const [showAdd, setShowAdd] = useState(false);
|
|
237
245
|
const [prefill, setPrefill] = useState(null);
|
|
238
246
|
const [probeById, setProbeById] = useState(null);
|
|
239
|
-
const
|
|
247
|
+
const orgQuery = useOrg();
|
|
248
|
+
const { data: org } = orgQuery;
|
|
240
249
|
const syncSecret = useSyncA2ASecret();
|
|
250
|
+
const canManageSharedAgents = !orgQuery.isLoading &&
|
|
251
|
+
!orgQuery.isError &&
|
|
252
|
+
(!org?.orgId || org.role === "owner" || org.role === "admin");
|
|
241
253
|
// Landing from a peer's "register back" deep link (see
|
|
242
254
|
// buildPeerRegisterBackLink): the open route only echoes `f_*` params onto
|
|
243
255
|
// the redirect URL, so read those here and open the Add popover prefilled.
|
|
@@ -343,26 +355,30 @@ export function AgentsSection() {
|
|
|
343
355
|
url,
|
|
344
356
|
color: "#6B7280",
|
|
345
357
|
}, null, 2);
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
return false;
|
|
358
|
+
const res = await fetch(agentNativePath("/_agent-native/resources"), {
|
|
359
|
+
method: "POST",
|
|
360
|
+
headers: { "Content-Type": "application/json" },
|
|
361
|
+
body: JSON.stringify({
|
|
362
|
+
path: remoteAgentResourcePath(id),
|
|
363
|
+
content: agentJson,
|
|
364
|
+
shared: true,
|
|
365
|
+
}),
|
|
366
|
+
});
|
|
367
|
+
if (!res.ok) {
|
|
368
|
+
let body;
|
|
369
|
+
try {
|
|
370
|
+
body = (await res.json());
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
throw new Error(`Add failed (${res.status})`);
|
|
374
|
+
}
|
|
375
|
+
throw new Error(body?.error ?? body?.message ?? `Add failed (${res.status})`);
|
|
365
376
|
}
|
|
377
|
+
// Deliberately don't close the popover here — a successful add shows a
|
|
378
|
+
// follow-up state (registration is one-way; the peer doesn't know
|
|
379
|
+
// about us yet) that the user dismisses explicitly.
|
|
380
|
+
fetchAgents();
|
|
381
|
+
return true;
|
|
366
382
|
};
|
|
367
383
|
const handleSave = async (agent) => {
|
|
368
384
|
const agentJson = JSON.stringify({
|
|
@@ -398,13 +414,13 @@ export function AgentsSection() {
|
|
|
398
414
|
}
|
|
399
415
|
catch { }
|
|
400
416
|
};
|
|
401
|
-
return (_jsxs("div", { children: [_jsx("div", { className: "mb-3 flex flex-wrap items-center justify-end gap-3", children: _jsxs("div", { className: "relative", children: [_jsxs("button", { onClick: () => {
|
|
417
|
+
return (_jsxs("div", { children: [_jsx("div", { className: "mb-3 flex flex-wrap items-center justify-end gap-3", children: canManageSharedAgents ? (_jsxs("div", { className: "relative", children: [_jsxs("button", { onClick: () => {
|
|
402
418
|
setShowAdd(!showAdd);
|
|
403
419
|
setEditingAgent(null);
|
|
404
420
|
}, className: "inline-flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs font-medium text-foreground hover:bg-accent", children: [showAdd ? _jsx(IconX, { size: 13 }) : _jsx(IconPlus, { size: 13 }), showAdd ? "Cancel" : "Connect agent"] }), showAdd && (_jsx(AgentAddPopover, { initialName: prefill?.name, initialUrl: prefill?.url, initialDescription: prefill?.description, secretSet: org?.a2aSecretSet, syncSecret: syncSecret, onAdd: handleAdd, onClose: () => {
|
|
405
421
|
setShowAdd(false);
|
|
406
422
|
setPrefill(null);
|
|
407
|
-
} }))] }) }), _jsx(A2ASecretStatusRow, { org: org, syncSecret: syncSecret }), loading ? (_jsxs("div", { className: "space-y-1.5", role: "status", "aria-busy": "true", "aria-label": "Loading connected agents", children: [_jsx(Skeleton, { className: "h-6 w-full bg-muted/50" }), _jsx(Skeleton, { className: "h-6 w-3/4 bg-muted/50" })] })) : agents.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center rounded-xl border border-border/70 bg-card px-5 py-8 text-center", children: [_jsx("span", { className: "mb-2 flex size-9 items-center justify-center rounded-full bg-muted text-muted-foreground", children: _jsx(IconTopologyRing2, { size: 17 }) }), _jsx("p", { className: "text-sm font-medium text-foreground", children: "No connected agents yet" }), _jsx("p", { className: "mt-1 max-w-sm text-xs leading-5 text-muted-foreground", children: "Connect an A2A agent to delegate work from chat." }), _jsxs("button", { onClick: () => setShowAdd(true), className: "mt-4 inline-flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs font-medium text-foreground hover:bg-accent", children: [_jsx(IconPlus, { size: 13 }), "Connect agent"] })] })) : (_jsx("div", { className: "overflow-hidden rounded-xl border border-border/70 bg-card text-card-foreground", children: _jsx("div", { className: "divide-y divide-border/60 px-4", children: agents.map((agent) => {
|
|
423
|
+
} }))] })) : !orgQuery.isLoading ? (_jsx("span", { className: "text-[10px] text-muted-foreground", children: "Only workspace owners and admins can connect agents." })) : null }), _jsx(A2ASecretStatusRow, { org: org, syncSecret: syncSecret }), loading ? (_jsxs("div", { className: "space-y-1.5", role: "status", "aria-busy": "true", "aria-label": "Loading connected agents", children: [_jsx(Skeleton, { className: "h-6 w-full bg-muted/50" }), _jsx(Skeleton, { className: "h-6 w-3/4 bg-muted/50" })] })) : agents.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center rounded-xl border border-border/70 bg-card px-5 py-8 text-center", children: [_jsx("span", { className: "mb-2 flex size-9 items-center justify-center rounded-full bg-muted text-muted-foreground", children: _jsx(IconTopologyRing2, { size: 17 }) }), _jsx("p", { className: "text-sm font-medium text-foreground", children: "No connected agents yet" }), _jsx("p", { className: "mt-1 max-w-sm text-xs leading-5 text-muted-foreground", children: "Connect an A2A agent to delegate work from chat." }), canManageSharedAgents ? (_jsxs("button", { onClick: () => setShowAdd(true), className: "mt-4 inline-flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs font-medium text-foreground hover:bg-accent", children: [_jsx(IconPlus, { size: 13 }), "Connect agent"] })) : !orgQuery.isLoading ? (_jsx("p", { className: "mt-4 text-xs text-muted-foreground", children: "Ask a workspace owner or admin to connect an agent." })) : null] })) : (_jsx("div", { className: "overflow-hidden rounded-xl border border-border/70 bg-card text-card-foreground", children: _jsx("div", { className: "divide-y divide-border/60 px-4", children: agents.map((agent) => {
|
|
408
424
|
const probe = probeById?.get(getRemoteAgentIdFromPath(agent.path).toLowerCase());
|
|
409
425
|
const dotState = !probe
|
|
410
426
|
? null
|
|
@@ -413,9 +429,9 @@ export function AgentsSection() {
|
|
|
413
429
|
: "warn";
|
|
414
430
|
return (_jsxs("div", { className: "group relative", children: [_jsxs("div", { className: "flex w-full items-center gap-3 py-4", children: [_jsx("span", { className: "flex size-8 shrink-0 items-center justify-center rounded-lg border border-border bg-background text-muted-foreground", children: _jsx(IconTopologyRing2, { size: 15 }) }), dotState ? (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { className: `h-1.5 w-1.5 shrink-0 rounded-full ${dotState === "ok"
|
|
415
431
|
? "bg-primary"
|
|
416
|
-
: "bg-destructive"}` }) }), _jsx(TooltipContent, { children: probe && describeProbeTooltip(probe) })] })) : (_jsx("span", { className: "h-1.5 w-1.5 shrink-0 rounded-full bg-transparent" })), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-sm font-medium text-foreground", children: agent.name }), _jsx("span", { className: "mt-0.5 block truncate text-xs text-muted-foreground", children: agent.url })] }), _jsx("button", { onClick: () => {
|
|
432
|
+
: "bg-destructive"}` }) }), _jsx(TooltipContent, { children: probe && describeProbeTooltip(probe) })] })) : (_jsx("span", { className: "h-1.5 w-1.5 shrink-0 rounded-full bg-transparent" })), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-sm font-medium text-foreground", children: agent.name }), _jsx("span", { className: "mt-0.5 block truncate text-xs text-muted-foreground", children: agent.url })] }), canManageSharedAgents ? (_jsx("button", { onClick: () => {
|
|
417
433
|
setEditingAgent(editingAgent === agent.id ? null : agent.id);
|
|
418
434
|
setShowAdd(false);
|
|
419
|
-
}, className: "shrink-0 rounded-md border border-border px-2.5 py-1.5 text-xs font-medium text-foreground hover:bg-accent", children: "Manage" })] }), editingAgent === agent.id && (_jsx(AgentEditPopover, { agent: agent, onSave: handleSave, onDelete: handleDelete, onClose: () => setEditingAgent(null) }))] }, agent.id));
|
|
435
|
+
}, className: "shrink-0 rounded-md border border-border px-2.5 py-1.5 text-xs font-medium text-foreground hover:bg-accent", children: "Manage" })) : null] }), canManageSharedAgents && editingAgent === agent.id && (_jsx(AgentEditPopover, { agent: agent, onSave: handleSave, onDelete: handleDelete, onClose: () => setEditingAgent(null) }))] }, agent.id));
|
|
420
436
|
}) }) }))] }));
|
|
421
437
|
}
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
|
|
|
26
26
|
* Body: { update: string (base64), requestSource?: string }
|
|
27
27
|
*/
|
|
28
28
|
export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
29
|
-
error: string;
|
|
30
29
|
ok?: undefined;
|
|
30
|
+
error: string;
|
|
31
31
|
} | {
|
|
32
32
|
error?: undefined;
|
|
33
33
|
ok: boolean;
|
|
@@ -42,22 +42,22 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
42
42
|
avgEvalScore: number;
|
|
43
43
|
} | {
|
|
44
44
|
error?: undefined;
|
|
45
|
+
ok?: undefined;
|
|
45
46
|
summary: import("./types.js").TraceSummary;
|
|
46
47
|
spans: import("./types.js").TraceSpan[];
|
|
47
48
|
id?: undefined;
|
|
48
|
-
ok?: undefined;
|
|
49
49
|
} | {
|
|
50
50
|
error?: undefined;
|
|
51
|
+
ok?: undefined;
|
|
51
52
|
summary?: undefined;
|
|
52
53
|
spans?: undefined;
|
|
53
54
|
id: string;
|
|
54
|
-
ok?: undefined;
|
|
55
55
|
} | {
|
|
56
|
+
ok?: undefined;
|
|
56
57
|
summary?: undefined;
|
|
57
58
|
spans?: undefined;
|
|
58
59
|
id?: undefined;
|
|
59
60
|
error: any;
|
|
60
|
-
ok?: undefined;
|
|
61
61
|
} | {
|
|
62
62
|
error?: undefined;
|
|
63
63
|
summary?: undefined;
|
|
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
|
|
|
48
48
|
}>;
|
|
49
49
|
/** DELETE /_agent-native/resources/:id — delete a resource */
|
|
50
50
|
export declare function handleDeleteResource(event: any): Promise<{
|
|
51
|
-
error: string;
|
|
52
51
|
ok?: undefined;
|
|
52
|
+
error: string;
|
|
53
53
|
} | {
|
|
54
54
|
error?: undefined;
|
|
55
55
|
ok: boolean;
|
package/dist/secrets/routes.d.ts
CHANGED
|
@@ -34,37 +34,37 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
|
|
|
34
34
|
/** POST /_agent-native/secrets/:key — write a secret. */
|
|
35
35
|
export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
36
36
|
error: string;
|
|
37
|
-
status?: undefined;
|
|
38
37
|
ok?: undefined;
|
|
38
|
+
status?: undefined;
|
|
39
39
|
} | {
|
|
40
|
+
error?: undefined;
|
|
40
41
|
ok: boolean;
|
|
41
42
|
status: string;
|
|
42
|
-
error?: undefined;
|
|
43
43
|
} | {
|
|
44
|
+
ok?: undefined;
|
|
44
45
|
error: string;
|
|
45
46
|
removed?: undefined;
|
|
46
|
-
ok?: undefined;
|
|
47
47
|
} | {
|
|
48
|
+
error?: undefined;
|
|
48
49
|
ok: boolean;
|
|
49
50
|
removed: boolean;
|
|
50
|
-
error?: undefined;
|
|
51
51
|
}>>;
|
|
52
52
|
/**
|
|
53
53
|
* POST /_agent-native/secrets/:key/test — validate an optional candidate value
|
|
54
54
|
* or the current stored value without changing anything.
|
|
55
55
|
*/
|
|
56
56
|
export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
57
|
+
ok?: undefined;
|
|
57
58
|
error: string;
|
|
58
59
|
note?: undefined;
|
|
59
|
-
ok?: undefined;
|
|
60
60
|
} | {
|
|
61
|
+
error?: undefined;
|
|
61
62
|
ok: boolean;
|
|
62
63
|
note?: undefined;
|
|
63
|
-
error?: undefined;
|
|
64
64
|
} | {
|
|
65
|
+
error?: undefined;
|
|
65
66
|
ok: boolean;
|
|
66
67
|
note: string;
|
|
67
|
-
error?: undefined;
|
|
68
68
|
} | {
|
|
69
69
|
note?: undefined;
|
|
70
70
|
ok: boolean;
|
|
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
|
|
|
95
95
|
export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
|
|
96
96
|
error: string;
|
|
97
97
|
} | {
|
|
98
|
+
error?: undefined;
|
|
98
99
|
ok: boolean;
|
|
99
100
|
key: string;
|
|
100
|
-
error?: undefined;
|
|
101
101
|
} | {
|
|
102
|
+
error?: undefined;
|
|
102
103
|
ok: boolean;
|
|
103
104
|
removed: boolean;
|
|
104
|
-
error?: undefined;
|
|
105
105
|
}>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.153.
|
|
3
|
+
"version": "0.153.4",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|