@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.
Files changed (34) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/AGENTS.md +4 -0
  3. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -0
  4. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +9 -6
  5. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +70 -10
  6. package/corpus/templates/forms/AGENTS.md +6 -0
  7. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -0
  8. package/corpus/templates/mail/AGENTS.md +5 -4
  9. package/corpus/templates/mail/actions/get-automation-settings.ts +17 -6
  10. package/corpus/templates/mail/actions/send-email.ts +17 -9
  11. package/corpus/templates/mail/actions/send-queued-drafts.ts +21 -11
  12. package/corpus/templates/mail/actions/update-automation-settings.ts +20 -6
  13. package/corpus/templates/mail/app/i18n/en-US.ts +5 -0
  14. package/corpus/templates/mail/app/pages/SettingsPage.tsx +68 -7
  15. package/corpus/templates/mail/server/lib/automation-settings.ts +27 -0
  16. package/corpus/templates/mail/server/plugins/agent-chat.ts +2 -0
  17. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +3 -3
  18. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +17 -4
  19. package/corpus/templates/slides/app/context/DeckContext.tsx +3 -2
  20. package/corpus/templates/slides/app/pages/DeckEditor.tsx +44 -0
  21. package/corpus/templates/slides/changelog/2026-08-12-arrow-key-navigation-in-the-slide-film-strip-now-scrolls-sli.md +6 -0
  22. package/corpus/templates/slides/changelog/2026-08-12-duplicate-a-slide-from-the-film-strip-with-cmd-ctrl-c-then-c.md +6 -0
  23. package/corpus/templates/slides/changelog/2026-08-12-presenting-a-deck-now-returns-you-to-the-slide-you-were-view.md +6 -0
  24. package/dist/client/resources/mcp-integration-catalog.d.ts +7 -0
  25. package/dist/client/resources/mcp-integration-catalog.js +14 -1
  26. package/dist/client/settings/AgentsSection.js +46 -30
  27. package/dist/collab/routes.d.ts +1 -1
  28. package/dist/notifications/routes.d.ts +1 -1
  29. package/dist/observability/routes.d.ts +3 -3
  30. package/dist/progress/routes.d.ts +1 -1
  31. package/dist/resources/handlers.d.ts +1 -1
  32. package/dist/secrets/routes.d.ts +9 -9
  33. package/dist/server/agent-chat-plugin.js +1 -0
  34. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8185
34
+ - template files: 8189
@@ -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: Some(width),
4510
- height: Some(height),
4509
+ width,
4510
+ height,
4511
4511
  bytes,
4512
4512
  has_audio: include_mic || include_system_audio,
4513
4513
  mic_captured: include_mic,
@@ -4532,8 +4532,11 @@ pub(crate) fn clear_shared_clip_recording(app: &AppHandle, recording_id: &str, p
4532
4532
  let saved = read_saved_recording_metadata(app, recording_id).ok();
4533
4533
  if let Some(saved) = saved {
4534
4534
  clear_saved_recording_after_success(app, &saved);
4535
- } else if let Err(error) = remove_saved_file(path, "shared Clip temporary artifact") {
4536
- eprintln!("[clips-tray] shared Clip cleanup failed: {error}");
4535
+ } else {
4536
+ remove_recording_intent(path);
4537
+ if let Err(error) = remove_saved_file(path, "shared Clip temporary artifact") {
4538
+ eprintln!("[clips-tray] shared Clip cleanup failed: {error}");
4539
+ }
4537
4540
  }
4538
4541
  let _ = app.emit("clips:pending-uploads-changed", ());
4539
4542
  }
@@ -1038,6 +1038,7 @@ fn materialize(
1038
1038
  label: &str,
1039
1039
  include_mic: bool,
1040
1040
  include_system_audio: bool,
1041
+ recovery: Option<(&str, &str, bool)>,
1041
1042
  ) -> Result<FinalizedNativeArtifact, String> {
1042
1043
  let active = take_active(state)?;
1043
1044
  let mut intervals = active.intervals.clone();
@@ -1112,10 +1113,25 @@ fn materialize(
1112
1113
  }
1113
1114
  let output = artifact_path(app, label)?;
1114
1115
  let audio = select_audio(&active.sources, include_mic, include_system_audio)?;
1115
- native_screen::materialize_mp4_slices_exact(&slices, &output, audio)?;
1116
1116
  let first = first_segment
1117
1117
  .as_ref()
1118
1118
  .ok_or_else(|| "no Rewind media selected".to_string())?;
1119
+ if let Some((server_url, recording_id, has_camera)) = recovery {
1120
+ native_screen::persist_recording_intent(
1121
+ &output,
1122
+ recording_id,
1123
+ server_url,
1124
+ native_screen::MP4_RECORDING_MIME_TYPE,
1125
+ first.width,
1126
+ first.height,
1127
+ include_mic || include_system_audio,
1128
+ include_mic,
1129
+ include_system_audio,
1130
+ has_camera,
1131
+ true,
1132
+ )?;
1133
+ }
1134
+ native_screen::materialize_mp4_slices_exact(&slices, &output, audio)?;
1119
1135
  Ok(FinalizedNativeArtifact::rewind_mp4(
1120
1136
  output,
1121
1137
  duration_ms,
@@ -1199,8 +1215,8 @@ pub(crate) async fn rewind_clip_stop_and_upload(
1199
1215
  &server_url,
1200
1216
  &recording_id,
1201
1217
  logical_duration_ms as u128,
1202
- sink_width,
1203
- sink_height,
1218
+ Some(sink_width),
1219
+ Some(sink_height),
1204
1220
  include_mic,
1205
1221
  include_system_audio,
1206
1222
  has_camera,
@@ -1230,8 +1246,8 @@ pub(crate) async fn rewind_clip_stop_and_upload(
1230
1246
  &server_url,
1231
1247
  &recording_id,
1232
1248
  result.duration_ms as u128,
1233
- result.width,
1234
- result.height,
1249
+ Some(result.width),
1250
+ Some(result.height),
1235
1251
  include_mic,
1236
1252
  include_system_audio,
1237
1253
  has_camera,
@@ -1257,8 +1273,8 @@ pub(crate) async fn rewind_clip_stop_and_upload(
1257
1273
  &server_url,
1258
1274
  &recording_id,
1259
1275
  result.duration_ms as u128,
1260
- result.width,
1261
- result.height,
1276
+ Some(result.width),
1277
+ Some(result.height),
1262
1278
  include_mic,
1263
1279
  include_system_audio,
1264
1280
  has_camera,
@@ -1314,19 +1330,62 @@ pub(crate) async fn rewind_clip_stop_and_upload(
1314
1330
  &recording_id,
1315
1331
  include_mic,
1316
1332
  include_system_audio,
1333
+ Some((&server_url, &recording_id, has_camera)),
1334
+ )?;
1335
+ native_screen::persist_shared_clip_recording(
1336
+ &app,
1337
+ &artifact.path,
1338
+ &server_url,
1339
+ &recording_id,
1340
+ artifact.duration_ms,
1341
+ artifact.width,
1342
+ artifact.height,
1343
+ include_mic,
1344
+ include_system_audio,
1345
+ has_camera,
1346
+ None,
1317
1347
  )?;
1318
- native_screen::upload_finalized_native_artifact(
1348
+ let recovery_server_url = server_url.clone();
1349
+ let recovery_recording_id = recording_id.clone();
1350
+ let artifact_path = artifact.path.clone();
1351
+ let result = native_screen::upload_finalized_native_artifact(
1319
1352
  &app,
1320
1353
  &artifact,
1321
1354
  server_url,
1322
- recording_id,
1355
+ recording_id.clone(),
1323
1356
  auth_token.unwrap_or_default(),
1324
1357
  cookie.unwrap_or_default(),
1325
1358
  NativeUploadMode::from_option(upload_mode),
1326
1359
  include_mic || include_system_audio,
1327
1360
  has_camera,
1328
1361
  )
1329
- .await
1362
+ .await;
1363
+ match &result {
1364
+ Ok(upload) if !upload.verification_pending => {
1365
+ native_screen::clear_shared_clip_recording(
1366
+ &app,
1367
+ &recovery_recording_id,
1368
+ &artifact_path,
1369
+ );
1370
+ }
1371
+ Err(error) => {
1372
+ let _ = native_screen::persist_shared_clip_recording(
1373
+ &app,
1374
+ &artifact_path,
1375
+ &recovery_server_url,
1376
+ &recovery_recording_id,
1377
+ artifact.duration_ms,
1378
+ artifact.width,
1379
+ artifact.height,
1380
+ include_mic,
1381
+ include_system_audio,
1382
+ has_camera,
1383
+ Some(error),
1384
+ );
1385
+ }
1386
+ _ => {}
1387
+ }
1388
+ result
1330
1389
  }
1331
1390
 
1332
1391
  #[tauri::command]
@@ -1390,6 +1449,7 @@ pub(crate) async fn rewind_clip_stop_and_save(
1390
1449
  &folder_name,
1391
1450
  include_mic,
1392
1451
  include_system_audio,
1452
+ None,
1393
1453
  )?;
1394
1454
  native_screen::save_finalized_native_artifact_to_local_export(
1395
1455
  &app,
@@ -32,6 +32,12 @@ Read the relevant skill before deeper work:
32
32
  combined dashboard/report requests.
33
33
  - For form setup/configuration previews, call `preview-form`. It returns a
34
34
  native inline summary/table and an "Open editor" expansion path.
35
+ - For product usage, agent-native signup, conversion, app-wide event, or other
36
+ data-owned-by-sibling questions, use `describe-workspace-apps` when ownership
37
+ is unclear, then delegate a narrow natural-language question with `call-agent`
38
+ to the owning app. In workspaces with Analytics, it normally owns first-party
39
+ signup, conversion, and app-usage metrics. Do not invent SQL or query another
40
+ app's database.
35
41
  - For an anonymous feedback form or survey, create all fields in one
36
42
  `create-form` call with `status: "published"`, verify the persisted form, and
37
43
  copy the returned `publicUrl` verbatim. Never derive the link from `slug` or
@@ -25,6 +25,7 @@ Core rules:
25
25
  - Use the framework sharing actions for form and response resources.
26
26
  - Use \`tool-search\` with a specific query when you need a less-common capability such as sharing, extensions, resources, memory, or advanced settings. Search results load matching tool schemas into the next step.
27
27
  - If a tool fails or returns no data, say that plainly and pick the next safe action. Never fabricate success from a tool error.
28
+ - When a question is about product usage, agent-native signups, conversions, app-wide events, or data owned by another workspace app, use \`describe-workspace-apps\` if ownership is unclear, then delegate a narrow natural-language question with \`call-agent\` to the owning app. In workspaces with Analytics, it normally owns first-party signup, conversion, and app-usage metrics. Do not invent SQL or query another app's database.
28
29
 
29
30
  Form UX guidance: keep forms focused with clear labels, sensible validation, minimal required fields, and progressive disclosure for advanced settings.`;
30
31
 
@@ -26,9 +26,10 @@ Read the relevant skill before deeper work:
26
26
  is connected, synthetic `local-emails` data otherwise. Call actions the same
27
27
  way either way, and never claim fallback data or a fallback send touched the
28
28
  user's real inbox.
29
- - Never send mail unless the user explicitly asks. Draft or queue for review by
30
- default; `send-email` is `needsApproval: true`. Use `queue-email-draft` for
31
- teammate or Slack-originated send requests.
29
+ - Interactive sends require explicit user approval. Draft or queue for review by
30
+ default; automation-triggered sends remain approval-gated unless the owner
31
+ opts into Mail's "Allow automations to send emails automatically" setting.
32
+ Use `queue-email-draft` for teammate or Slack-originated send requests.
32
33
  - Resolve people with `find-contact` before drafting or sending. Never guess an
33
34
  address pattern; if it returns zero matches, tell the user.
34
35
  - Read `get-mail-settings` before drafting. Use the configured `signature`
@@ -66,7 +67,7 @@ Read the relevant skill before deeper work:
66
67
  | `get-hubspot-contact` | HubSpot contact + deals + tickets by email. |
67
68
  | `create-attachment-upload` | Short-lived upload URL for an attachment. |
68
69
  | `manage-draft` | Create/update/delete a `compose-{id}` draft. |
69
- | `send-email` | Real send; `needsApproval: true`. |
70
+ | `send-email` | Real send; interactive calls require approval, while automation calls require the Mail opt-in setting. |
70
71
  | `queue-email-draft` / `list-queued-drafts` / `update-queued-draft` / `open-queued-draft` / `send-queued-drafts` | Teammate/Slack draft review. |
71
72
  | `mark-read` / `mark-thread-read` / `star-email` / `archive-email` / `unarchive-email` / `trash-email` / `untrash-email` / `move-email` | Message/thread state; `mark-read` does bulk cleanup. |
72
73
  | `send-scheduled-email-now` / `cancel-scheduled-email` | Send or cancel a scheduled send. |
@@ -4,10 +4,14 @@ import { getUserSetting } from "@agent-native/core/settings";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { resolveAutomationModelSettings } from "../server/lib/automation-model.js";
7
+ import {
8
+ allowsAutomationSends,
9
+ type MailAutomationSettings,
10
+ } from "../server/lib/automation-settings.js";
7
11
 
8
12
  export default defineAction({
9
13
  description:
10
- "Read the engine and model used to evaluate inbox automation rules, falling back to the app's configured agent engine.",
14
+ "Read the engine, model, and email-send permission used by inbox automations, falling back to the app's configured agent engine.",
11
15
  schema: z.object({}),
12
16
  http: { method: "GET" },
13
17
  agentTool: false,
@@ -15,10 +19,17 @@ export default defineAction({
15
19
  const ownerEmail = getRequestUserEmail();
16
20
  if (!ownerEmail) throw new Error("Unauthenticated");
17
21
 
18
- const data = (await getUserSetting(ownerEmail, "automation-settings")) as {
19
- engine?: string;
20
- model?: string;
21
- } | null;
22
- return resolveAutomationModelSettings(ownerEmail, data);
22
+ const data = (await getUserSetting(
23
+ ownerEmail,
24
+ "automation-settings",
25
+ )) as MailAutomationSettings | null;
26
+ const modelSettings = await resolveAutomationModelSettings(
27
+ ownerEmail,
28
+ data,
29
+ );
30
+ return {
31
+ ...modelSettings,
32
+ allowAutomationSends: allowsAutomationSends(data),
33
+ };
23
34
  },
24
35
  });
@@ -1,4 +1,5 @@
1
1
  import { defineAction } from "@agent-native/core";
2
+ import type { ActionRunContext } from "@agent-native/core/action";
2
3
  import { writeAppState } from "@agent-native/core/application-state";
3
4
  import { emit } from "@agent-native/core/event-bus";
4
5
  import { setOAuthDisplayName } from "@agent-native/core/oauth-tokens";
@@ -8,6 +9,7 @@ import { getUserSetting } from "@agent-native/core/settings";
8
9
  import { nanoid } from "nanoid";
9
10
  import { z } from "zod";
10
11
 
12
+ import { requiresEmailSendApproval } from "../server/lib/automation-settings.js";
11
13
  import {
12
14
  collectLinks,
13
15
  newClickToken,
@@ -109,7 +111,7 @@ const attachmentSchema = z.object({
109
111
 
110
112
  export default defineAction({
111
113
  description:
112
- "Send an email via Gmail. IMPORTANT: Never call this unless the user explicitly asks to send — always draft first and show the content to the user for review before sending.",
114
+ "Send an email via Gmail. Interactive sends require approval. Automation-triggered sends require the owner to opt in through Mail settings; otherwise they remain approval-gated.",
113
115
  schema: z.object({
114
116
  to: z.string().describe("Recipient email(s), comma-separated"),
115
117
  subject: z.string().describe("Email subject"),
@@ -135,16 +137,22 @@ export default defineAction({
135
137
  "Files to attach. Each entry must reference a previously-uploaded file by its server-side `filename`. The upload must have been created via the media-upload endpoint before calling this action.",
136
138
  ),
137
139
  }),
138
- // Human-in-the-loop gate: actually sending an email is outward-facing and
139
- // hard to undo, so the agent can never send without a human approving the
140
- // specific call. The loop pauses with `approval_required`; the user approves
141
- // before the message goes out. Drafting/queueing is unaffected — only the
142
- // real send is gated. This is the canonical (and intentionally rare) use of
143
- // `needsApproval` in the framework.
144
- needsApproval: true,
145
- run: async (args) => {
140
+ // Interactive sends stay human-approved. Event-triggered automations may
141
+ // opt out through the owner's Mail Automation settings, which are read from
142
+ // trusted action context rather than from tool input.
143
+ needsApproval: (_args, ctx?: ActionRunContext) =>
144
+ requiresEmailSendApproval(ctx),
145
+ run: async (args, ctx) => {
146
146
  const ownerEmail = getRequestUserEmail();
147
147
  if (!ownerEmail) throw new Error("no authenticated user");
148
+ if (
149
+ ctx?.caller === "automation" &&
150
+ (await requiresEmailSendApproval(ctx))
151
+ ) {
152
+ throw new Error(
153
+ "Automation email sending is disabled. Enable it in Mail settings to send automatically.",
154
+ );
155
+ }
148
156
  const settings = await readSettings();
149
157
 
150
158
  // Resolve attachments eagerly — fail before touching Gmail if any are missing.
@@ -1,6 +1,8 @@
1
1
  import { defineAction } from "@agent-native/core";
2
+ import type { ActionRunContext } from "@agent-native/core/action";
2
3
  import { z } from "zod";
3
4
 
5
+ import { requiresEmailSendApproval } from "../server/lib/automation-settings.js";
4
6
  import {
5
7
  claimQueuedDraftForSending,
6
8
  listQueuedDrafts,
@@ -32,7 +34,10 @@ type SendOutcome =
32
34
  | { outcome: "skipped"; id: string; reason: string }
33
35
  | { outcome: "failed"; id: string; error: string };
34
36
 
35
- async function sendOne(id: string): Promise<SendOutcome> {
37
+ async function sendOne(
38
+ id: string,
39
+ actionContext?: ActionRunContext,
40
+ ): Promise<SendOutcome> {
36
41
  const claim = await claimQueuedDraftForSending(id);
37
42
  if (!claim.claimed) {
38
43
  if (claim.reason === "sent") {
@@ -50,14 +55,17 @@ async function sendOne(id: string): Promise<SendOutcome> {
50
55
 
51
56
  const { ctx, draft, claimId, priorStatus } = claim;
52
57
  try {
53
- const result = await (sendEmailAction as any).run({
54
- to: draft.to,
55
- cc: draft.cc || undefined,
56
- bcc: draft.bcc || undefined,
57
- subject: draft.subject,
58
- body: draft.body,
59
- account: draft.accountEmail || undefined,
60
- });
58
+ const result = await sendEmailAction.run(
59
+ {
60
+ to: draft.to,
61
+ cc: draft.cc || undefined,
62
+ bcc: draft.bcc || undefined,
63
+ subject: draft.subject,
64
+ body: draft.body,
65
+ account: draft.accountEmail || undefined,
66
+ },
67
+ actionContext,
68
+ );
61
69
 
62
70
  if (typeof result === "string" && result.startsWith("Error")) {
63
71
  throw new Error(result);
@@ -95,7 +103,9 @@ export default defineAction({
95
103
  .optional()
96
104
  .describe("Maximum drafts to send when all=true"),
97
105
  }),
98
- run: async (args) => {
106
+ needsApproval: (_args, ctx?: ActionRunContext) =>
107
+ requiresEmailSendApproval(ctx),
108
+ run: async (args, ctx) => {
99
109
  let ids: string[] = [];
100
110
 
101
111
  if (args.all) {
@@ -127,7 +137,7 @@ export default defineAction({
127
137
  const failed: Array<{ id: string; error: string }> = [];
128
138
 
129
139
  for (const id of ids) {
130
- const result = await sendOne(id);
140
+ const result = await sendOne(id, ctx);
131
141
  if (result.outcome === "sent") {
132
142
  sent.push(result);
133
143
  } else if (result.outcome === "failed") {
@@ -3,12 +3,18 @@ import { getRequestUserEmail } from "@agent-native/core/server";
3
3
  import { getUserSetting, putUserSetting } from "@agent-native/core/settings";
4
4
  import { z } from "zod";
5
5
 
6
+ import type { MailAutomationSettings } from "../server/lib/automation-settings.js";
7
+
6
8
  export default defineAction({
7
9
  description:
8
- "Set the engine and model used to evaluate inbox automation rules.",
10
+ "Set the engine and model used to evaluate inbox automation rules, and choose whether automations may send email without per-message approval.",
9
11
  schema: z.object({
10
12
  engine: z.string().optional().describe("Agent engine id"),
11
13
  model: z.string().optional().describe("Model id for that engine"),
14
+ allowAutomationSends: z
15
+ .boolean()
16
+ .optional()
17
+ .describe("Allow automation-triggered emails to send without approval"),
12
18
  }),
13
19
  http: { method: "PUT" },
14
20
  agentTool: false,
@@ -17,17 +23,25 @@ export default defineAction({
17
23
  if (!ownerEmail) throw new Error("Unauthenticated");
18
24
 
19
25
  const existing =
20
- ((await getUserSetting(ownerEmail, "automation-settings")) as {
21
- engine?: string;
22
- model?: string;
23
- } | null) || {};
26
+ ((await getUserSetting(
27
+ ownerEmail,
28
+ "automation-settings",
29
+ )) as MailAutomationSettings | null) || {};
24
30
  const updated = {
25
31
  ...existing,
26
32
  ...(args.engine ? { engine: args.engine } : {}),
27
33
  ...(args.model ? { model: args.model } : {}),
34
+ ...(args.allowAutomationSends !== undefined
35
+ ? { allowAutomationSends: args.allowAutomationSends }
36
+ : {}),
28
37
  };
29
38
  await putUserSetting(ownerEmail, "automation-settings", updated);
30
39
 
31
- return { success: true, engine: updated.engine, model: updated.model };
40
+ return {
41
+ success: true,
42
+ engine: updated.engine,
43
+ model: updated.model,
44
+ allowAutomationSends: updated.allowAutomationSends === true,
45
+ };
32
46
  },
33
47
  });
@@ -478,6 +478,11 @@ const messages = {
478
478
  lastRun: "Last run:",
479
479
  automationsDescription:
480
480
  "Rules that automatically process new inbox emails using AI.",
481
+ allowAutomationSends: "Allow automations to send emails automatically",
482
+ allowAutomationSendsDescription:
483
+ "Off by default. Turn this on when you want automations to send emails without asking for approval each time.",
484
+ automationSendSettingSaveFailed:
485
+ "Could not save the automation email setting.",
481
486
  loadingModels: "Loading models",
482
487
  newRule: "New rule",
483
488
  noAutomationRules: "No automation rules yet.",
@@ -868,6 +868,12 @@ function TriggersSubsection() {
868
868
 
869
869
  // ─── Automations Section ─────────────────────────────────────────────────────
870
870
 
871
+ type AutomationSettings = {
872
+ engine?: string;
873
+ model?: string;
874
+ allowAutomationSends: boolean;
875
+ };
876
+
871
877
  function AutomationsSection() {
872
878
  const t = useT();
873
879
  const { data: rules = [], isLoading } = useAutomations();
@@ -896,9 +902,9 @@ function AutomationsSection() {
896
902
  // Refetch on any settings write or agent action so agent-driven changes
897
903
  // (e.g. update-automation-settings) show up without a manual refresh.
898
904
  const settingsSync = useChangeVersions(["settings", "action"]);
899
- const { data: autoSettings } = useQuery({
905
+ const { data: autoSettings } = useQuery<AutomationSettings>({
900
906
  queryKey: ["automation-settings", settingsSync],
901
- queryFn: async () => {
907
+ queryFn: async (): Promise<AutomationSettings> => {
902
908
  try {
903
909
  return await callAction(
904
910
  "get-automation-settings",
@@ -906,7 +912,7 @@ function AutomationsSection() {
906
912
  { method: "GET" },
907
913
  );
908
914
  } catch {
909
- return { model: defaultModel };
915
+ return { model: defaultModel, allowAutomationSends: false };
910
916
  }
911
917
  },
912
918
  staleTime: 30_000,
@@ -914,6 +920,7 @@ function AutomationsSection() {
914
920
  });
915
921
 
916
922
  const queryClient = useQueryClient();
923
+ const [isSavingAutomationSends, setIsSavingAutomationSends] = useState(false);
917
924
  const selectedModel = autoSettings?.model || defaultModel;
918
925
  const selectedEngine =
919
926
  autoSettings?.engine ||
@@ -930,7 +937,13 @@ function AutomationsSection() {
930
937
  const handleModelChange = async (value: string) => {
931
938
  const [engine, model] = value.split("::");
932
939
  if (!engine || !model) return;
933
- queryClient.setQueryData(["automation-settings"], { engine, model });
940
+ queryClient.setQueriesData<AutomationSettings>(
941
+ { queryKey: ["automation-settings"] },
942
+ (current) =>
943
+ current
944
+ ? { ...current, engine, model }
945
+ : { engine, model, allowAutomationSends: false },
946
+ );
934
947
  await callAction(
935
948
  "update-automation-settings",
936
949
  { engine, model },
@@ -938,6 +951,40 @@ function AutomationsSection() {
938
951
  );
939
952
  };
940
953
 
954
+ const handleAutomationSendsChange = async (enabled: boolean) => {
955
+ if (!autoSettings || isSavingAutomationSends) return;
956
+ const previous = autoSettings.allowAutomationSends;
957
+ queryClient.setQueriesData<AutomationSettings>(
958
+ { queryKey: ["automation-settings"] },
959
+ (current) =>
960
+ current ? { ...current, allowAutomationSends: enabled } : current,
961
+ );
962
+ setIsSavingAutomationSends(true);
963
+ try {
964
+ await callAction(
965
+ "update-automation-settings",
966
+ { allowAutomationSends: enabled },
967
+ { method: "PUT" },
968
+ );
969
+ } catch (error) {
970
+ queryClient.setQueriesData<AutomationSettings>(
971
+ { queryKey: ["automation-settings"] },
972
+ (current) =>
973
+ current ? { ...current, allowAutomationSends: previous } : current,
974
+ );
975
+ toast.error(
976
+ error instanceof Error
977
+ ? error.message
978
+ : t("settings.automationSendSettingSaveFailed"),
979
+ );
980
+ } finally {
981
+ setIsSavingAutomationSends(false);
982
+ await queryClient.invalidateQueries({
983
+ queryKey: ["automation-settings"],
984
+ });
985
+ }
986
+ };
987
+
941
988
  const handleCreate = (data: {
942
989
  name: string;
943
990
  condition: string;
@@ -996,6 +1043,20 @@ function AutomationsSection() {
996
1043
  </Button>
997
1044
  </div>
998
1045
 
1046
+ <div className="max-w-2xl mb-6">
1047
+ {autoSettings ? (
1048
+ <SettingsSwitchRow
1049
+ title={t("settings.allowAutomationSends")}
1050
+ description={t("settings.allowAutomationSendsDescription")}
1051
+ checked={autoSettings.allowAutomationSends}
1052
+ disabled={isSavingAutomationSends}
1053
+ onCheckedChange={handleAutomationSendsChange}
1054
+ />
1055
+ ) : (
1056
+ <Skeleton className="h-16 w-full" />
1057
+ )}
1058
+ </div>
1059
+
999
1060
  {/* Content */}
1000
1061
  <div className="max-w-2xl space-y-2">
1001
1062
  {/* New rule form */}
@@ -1219,7 +1280,7 @@ function DraftingSection() {
1219
1280
  );
1220
1281
  }
1221
1282
 
1222
- function TrackingRow({
1283
+ function SettingsSwitchRow({
1223
1284
  title,
1224
1285
  description,
1225
1286
  checked,
@@ -1281,13 +1342,13 @@ function TrackingSection() {
1281
1342
  </>
1282
1343
  ) : (
1283
1344
  <>
1284
- <TrackingRow
1345
+ <SettingsSwitchRow
1285
1346
  title={t("settings.trackEmailOpens")}
1286
1347
  description={t("settings.trackEmailOpensDescription")}
1287
1348
  checked={tracking.opens}
1288
1349
  onCheckedChange={(v) => update({ opens: v })}
1289
1350
  />
1290
- <TrackingRow
1351
+ <SettingsSwitchRow
1291
1352
  title={t("settings.trackLinkClicks")}
1292
1353
  description={t("settings.trackLinkClicksDescription")}
1293
1354
  checked={tracking.clicks}
@@ -0,0 +1,27 @@
1
+ import type { ActionRunContext } from "@agent-native/core/action";
2
+ import { getUserSetting } from "@agent-native/core/settings";
3
+
4
+ export type MailAutomationSettings = {
5
+ engine?: string;
6
+ model?: string;
7
+ allowAutomationSends?: boolean;
8
+ };
9
+
10
+ export function allowsAutomationSends(settings: unknown): boolean {
11
+ return (
12
+ typeof settings === "object" &&
13
+ settings !== null &&
14
+ "allowAutomationSends" in settings &&
15
+ (settings as { allowAutomationSends?: unknown }).allowAutomationSends ===
16
+ true
17
+ );
18
+ }
19
+
20
+ export async function requiresEmailSendApproval(
21
+ ctx?: ActionRunContext,
22
+ ): Promise<boolean> {
23
+ if (ctx?.caller !== "automation" || !ctx.userEmail) return true;
24
+
25
+ const settings = await getUserSetting(ctx.userEmail, "automation-settings");
26
+ return !allowsAutomationSends(settings);
27
+ }
@@ -151,6 +151,8 @@ Be concise and helpful. When summarizing emails, include sender, subject, and a
151
151
  You can create and manage email automation rules that process new inbox emails automatically using AI.
152
152
  Use manage-automations to create rules like "auto-label newsletters", "star emails from my boss", etc.
153
153
 
154
+ Sending email from an automation is opt-in. Mail keeps "Allow automations to send emails automatically" off by default. When it is off, an automation may draft or queue an email, but a real send remains approval-gated. Turning it on lets event-triggered automations send without asking for approval each time; it does not remove approval from normal interactive sends.
155
+
154
156
  Examples:
155
157
  - User says "auto-label newsletters" \u2192 create rule with condition "from a newsletter or marketing mailing list" and action label:"newsletters"
156
158
  - User says "archive marketing emails" \u2192 create rule with condition "marketing or promotional email" and action archive
@@ -441,9 +441,9 @@ export default function EditorSidebar({
441
441
  const nextSlideId = slides[nextIndex].id;
442
442
  onSelectSlide(nextSlideId);
443
443
  requestAnimationFrame(() => {
444
- slideButtonRefs.current.get(nextSlideId)?.focus({
445
- preventScroll: true,
446
- });
444
+ const nextButton = slideButtonRefs.current.get(nextSlideId);
445
+ nextButton?.focus({ preventScroll: true });
446
+ nextButton?.scrollIntoView({ block: "nearest" });
447
447
  });
448
448
  }
449
449
  };
@@ -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 (isShared) {
469
- const token = deckId.replace("__shared__/", "");
479
+ if (isSharedRef.current) {
480
+ const token = deckIdRef.current.replace("__shared__/", "");
470
481
  navigate(`/share/${token}`);
471
482
  } else {
472
- navigate(`/deck/${deckId}`);
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) => void;
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
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-12
4
+ ---
5
+
6
+ Arrow-key navigation in the slide film strip now scrolls slides below the fold into view
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-08-12
4
+ ---
5
+
6
+ Duplicate a slide from the film strip with Cmd/Ctrl+C then Cmd/Ctrl+V
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-12
4
+ ---
5
+
6
+ Presenting a deck now returns you to the slide you were viewing when you exit
@@ -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) => [integration.name, ...(integration.brandAliases ?? [])].some((alias) => textContainsTerm(normalizedText, alias));
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
- const ok = await onAdd(trimmedName, trimmedUrl, trimmedDescription);
169
- if (ok) {
170
- setAdded({
171
- name: trimmedName,
172
- url: trimmedUrl,
173
- description: trimmedDescription,
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 { data: org } = useOrg();
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
- try {
347
- const res = await fetch(agentNativePath("/_agent-native/resources"), {
348
- method: "POST",
349
- headers: { "Content-Type": "application/json" },
350
- body: JSON.stringify({
351
- path: remoteAgentResourcePath(id),
352
- content: agentJson,
353
- shared: true,
354
- }),
355
- });
356
- // Deliberately don't close the popover here — a successful add shows a
357
- // follow-up state (registration is one-way; the peer doesn't know
358
- // about us yet) that the user dismisses explicitly.
359
- if (res.ok)
360
- fetchAgents();
361
- return res.ok;
362
- }
363
- catch {
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
  }
@@ -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;
@@ -28,6 +28,6 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
28
28
  } | {
29
29
  count?: undefined;
30
30
  updated?: undefined;
31
- ok: boolean;
32
31
  error?: undefined;
32
+ ok: boolean;
33
33
  }>>;
@@ -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;
@@ -15,6 +15,6 @@ export declare function createProgressHandler(): import("h3").EventHandlerWithFe
15
15
  error: string;
16
16
  ok?: undefined;
17
17
  } | {
18
- ok: boolean;
19
18
  error?: undefined;
19
+ ok: boolean;
20
20
  }>>;
@@ -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;
@@ -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
  }>>;
@@ -2285,6 +2285,7 @@ export function createAgentChatPlugin(options) {
2285
2285
  const leanActionEntries = {
2286
2286
  ...templateScripts,
2287
2287
  ...resourceScripts,
2288
+ ...workspaceFileActions,
2288
2289
  ...refreshScreenTool,
2289
2290
  ...urlTools,
2290
2291
  ...chatScripts,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.153.2",
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": {