@agent-native/core 0.176.1 → 0.177.0-nightly-20260828155142

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 (71) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/actions/set-workspace-feature-flag.ts +2 -2
  3. package/corpus/templates/analytics/app/components/agents/FeatureFlagsFleetPanel.tsx +16 -1
  4. package/corpus/templates/analytics/changelog/2026-08-27-show-analytics-s-own-registered-flags-in-fleet-feature-flag-.md +6 -0
  5. package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +204 -32
  6. package/corpus/templates/assets/changelog/2026-08-26-templates-across-brand-kits.md +1 -0
  7. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +61 -68
  8. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +9 -3
  9. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +3 -3
  10. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +8 -3
  11. package/corpus/templates/calendar/app/lib/calendar-timezone.ts +12 -3
  12. package/corpus/templates/calendar/changelog/2026-08-28-all-day-events-stay-in-the-compact-top-bar-in-day-and-week-v.md +6 -0
  13. package/corpus/templates/calendar/changelog/2026-08-28-event-creators-can-choose-whether-a-meeting-shows-as-free-or.md +6 -0
  14. package/corpus/templates/clips/app/global.css +0 -27
  15. package/corpus/templates/clips/app/i18n/en-US.ts +2 -0
  16. package/corpus/templates/clips/app/routes/download.tsx +131 -16
  17. package/corpus/templates/clips/app/routes/share.$shareId.tsx +35 -20
  18. package/corpus/templates/clips/server/routes/api/clips-latest.json.get.ts +77 -22
  19. package/corpus/templates/forms/actions/response-insights.ts +4 -4
  20. package/corpus/templates/forms/actions/retry-response-deliveries.ts +26 -0
  21. package/corpus/templates/forms/server/db/schema.ts +64 -17
  22. package/corpus/templates/forms/server/handlers/submissions.ts +1022 -94
  23. package/corpus/templates/forms/server/lib/integrations.ts +100 -33
  24. package/corpus/templates/forms/server/middleware/00-public-cors.ts +1 -1
  25. package/corpus/templates/forms/server/plugins/db.ts +40 -0
  26. package/corpus/templates/plan/agent-native.json +1 -4
  27. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +6 -1
  28. package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +2 -2
  29. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -6
  30. package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +78 -12
  31. package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +4 -4
  32. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +111 -27
  33. package/corpus/templates/slides/app/components/editor/SlidesLayersPanel.tsx +20 -34
  34. package/corpus/templates/slides/app/components/editor/slide-style.ts +2 -0
  35. package/corpus/templates/slides/app/global.css +35 -0
  36. package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +133 -5
  37. package/corpus/templates/slides/app/lib/slide-image-replacement.ts +675 -0
  38. package/corpus/templates/slides/app/lib/validate-native-slide-html.ts +1 -0
  39. package/corpus/templates/slides/app/pages/DeckEditor.tsx +44 -44
  40. package/corpus/templates/slides/app/root.tsx +1 -22
  41. package/corpus/templates/slides/changelog/2026-08-27-image-fit-controls-apply-cover-crops-and-position.md +6 -0
  42. package/corpus/templates/slides/changelog/2026-08-28-shared-slides-with-a-style-block-render-again.md +6 -0
  43. package/dist/action.d.ts +3 -1
  44. package/dist/client/FeedbackButton.d.ts +12 -0
  45. package/dist/client/FeedbackButton.js +62 -28
  46. package/dist/client/app-providers.js +13 -1
  47. package/dist/client/feedback-context.d.ts +1 -0
  48. package/dist/client/feedback-context.js +3 -2
  49. package/dist/client/host-bridge.d.ts +1 -0
  50. package/dist/client/observability/ThumbsFeedback.js +73 -10
  51. package/dist/client/webmcp.d.ts +4 -0
  52. package/dist/client/webmcp.js +51 -4
  53. package/dist/collab/routes.d.ts +1 -1
  54. package/dist/collab/struct-routes.d.ts +1 -1
  55. package/dist/index.browser.d.ts +1 -1
  56. package/dist/index.browser.js +1 -1
  57. package/dist/observability/routes.d.ts +5 -5
  58. package/dist/observability/routes.js +8 -2
  59. package/dist/observability/store.d.ts +1 -1
  60. package/dist/observability/store.js +18 -3
  61. package/dist/observability/types.d.ts +1 -0
  62. package/dist/progress/routes.d.ts +1 -1
  63. package/dist/resources/handlers.d.ts +2 -2
  64. package/dist/secrets/routes.d.ts +9 -9
  65. package/dist/server/action-routes.d.ts +1 -6
  66. package/dist/server/action-routes.js +58 -17
  67. package/dist/server/agent-chat-plugin.js +8 -1
  68. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  69. package/dist/server/transcribe-voice.d.ts +1 -1
  70. package/docs/content/webmcp.mdx +31 -12
  71. package/package.json +1 -1
@@ -129,15 +129,15 @@ import {
129
129
  applyOptimisticImagePreview,
130
130
  hasOptimisticImagePreview,
131
131
  imageFileLooksSupported,
132
+ insertDroppedImageIntoSlideHtml,
132
133
  replaceOptimisticImagePreview,
134
+ replaceImageTargetInSlideHtml,
133
135
  stripOptimisticImagePreviews,
136
+ updateImageFitInSlideHtml,
137
+ type ImageObjectPosition,
134
138
  type OptimisticImagePreview,
135
139
  type SlideImageDropPosition,
136
140
  } from "@/lib/slide-image-replacement";
137
- import {
138
- insertDroppedImageIntoSlideHtml,
139
- replaceImageTargetInSlideHtml,
140
- } from "@/lib/slide-image-replacement";
141
141
  import { TAB_ID } from "@/lib/tab-id";
142
142
  import {
143
143
  shouldActivateRectangleTool,
@@ -486,14 +486,6 @@ export default function DeckEditor() {
486
486
  // loading when `createdByMe` already confirms ownership — otherwise a
487
487
  // viewer would briefly see (and could click) edit affordances.
488
488
  const { canEdit, canComment } = useDeckRole(id, deck?.createdByMe === true);
489
- useEffect(() => {
490
- const handleToggleLayers = () => {
491
- if (canEdit) toggleLayers();
492
- };
493
- window.addEventListener("slides:toggle-layers", handleToggleLayers);
494
- return () =>
495
- window.removeEventListener("slides:toggle-layers", handleToggleLayers);
496
- }, [canEdit, toggleLayers]);
497
489
  const isNewDeckGenerating = shouldShowNewDeckGeneratingProgress({
498
490
  generating,
499
491
  isNewDeckCreation: wasNewDeckCreation.current,
@@ -1016,6 +1008,31 @@ export default function DeckEditor() {
1016
1008
  [id, replaceImageInSlide, updateSlide],
1017
1009
  );
1018
1010
 
1011
+ // Update fit or crop position on an image in the current slide
1012
+ const updateImageFit = useCallback(
1013
+ (
1014
+ imgSrc: string,
1015
+ updates: {
1016
+ objectFit?: "cover" | "contain";
1017
+ objectPosition?: ImageObjectPosition;
1018
+ },
1019
+ imageOccurrence?: number,
1020
+ ) => {
1021
+ if (!id || !currentSlideRef.current) return;
1022
+ const slide = currentSlideRef.current;
1023
+ const updatedContent = updateImageFitInSlideHtml(
1024
+ slide.content,
1025
+ imgSrc,
1026
+ updates,
1027
+ imageOccurrence,
1028
+ );
1029
+ if (updatedContent !== slide.content) {
1030
+ updateSlide(id, slide.id, { content: updatedContent });
1031
+ }
1032
+ },
1033
+ [id, updateSlide],
1034
+ );
1035
+
1019
1036
  const handleClipboardImagePaste = useCallback(
1020
1037
  (event: ClipboardEvent) => {
1021
1038
  if (!canEdit || event.defaultPrevented) return;
@@ -1050,39 +1067,21 @@ export default function DeckEditor() {
1050
1067
 
1051
1068
  // Toggle object-fit on an image in the current slide
1052
1069
  const toggleObjectFit = useCallback(
1053
- (imgSrc: string, newFit: string) => {
1054
- if (!id || !currentSlideRef.current) return;
1055
- const slide = currentSlideRef.current;
1056
- const escapedSrc = imgSrc.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1057
- // Match the img tag containing this src and update/add object-fit in its style
1058
- const imgRegex = new RegExp(
1059
- `(<img[^>]*src=["']${escapedSrc}["'][^>]*?)(/?>)`,
1060
- );
1061
- const match = slide.content.match(imgRegex);
1062
- if (!match) return;
1063
- let imgTag = match[1];
1064
- // Update or add style attribute with object-fit
1065
- if (/style\s*=\s*["']/.test(imgTag)) {
1066
- if (/object-fit\s*:/.test(imgTag)) {
1067
- imgTag = imgTag.replace(
1068
- /object-fit\s*:\s*[^;"']+/,
1069
- `object-fit: ${newFit}`,
1070
- );
1071
- } else {
1072
- imgTag = imgTag.replace(
1073
- /style\s*=\s*["']/,
1074
- `style="object-fit: ${newFit}; `,
1075
- );
1076
- }
1077
- } else {
1078
- imgTag += ` style="object-fit: ${newFit};"`;
1079
- }
1080
- const updatedContent = slide.content.replace(imgRegex, imgTag + match[2]);
1081
- if (updatedContent !== slide.content) {
1082
- updateSlide(id, slide.id, { content: updatedContent });
1083
- }
1070
+ (imgSrc: string, newFit: "cover" | "contain", imageOccurrence?: number) => {
1071
+ updateImageFit(imgSrc, { objectFit: newFit }, imageOccurrence);
1084
1072
  },
1085
- [id, updateSlide],
1073
+ [updateImageFit],
1074
+ );
1075
+
1076
+ const updateObjectPosition = useCallback(
1077
+ (
1078
+ imgSrc: string,
1079
+ objectPosition: ImageObjectPosition,
1080
+ imageOccurrence?: number,
1081
+ ) => {
1082
+ updateImageFit(imgSrc, { objectPosition }, imageOccurrence);
1083
+ },
1084
+ [updateImageFit],
1086
1085
  );
1087
1086
 
1088
1087
  // Handle direct file upload and replace image
@@ -2353,6 +2352,7 @@ export default function DeckEditor() {
2353
2352
  onDropImage={uploadAndApplyImage}
2354
2353
  onDropImageUrl={dropImageUrlOnSlide}
2355
2354
  onToggleObjectFit={toggleObjectFit}
2355
+ onChangeObjectPosition={updateObjectPosition}
2356
2356
  slideIndex={currentIndex >= 0 ? currentIndex : 0}
2357
2357
  designSystem={designSystem}
2358
2358
  aspectRatio={deck.aspectRatio}
@@ -223,28 +223,7 @@ function AppContent() {
223
223
  const [cmdkOpen, setCmdkOpen] = useState(false);
224
224
  const t = useT();
225
225
  const navigate = useNavigate();
226
- const handleCommandMenuShortcut = useCallback(() => {
227
- const editor = document.querySelector<HTMLElement>(
228
- "[data-slides-editor-root]",
229
- );
230
- const active = document.activeElement;
231
- const contentEditable =
232
- active instanceof HTMLElement &&
233
- (active.isContentEditable ||
234
- Boolean(active.closest("[contenteditable='true']")));
235
- const formControl =
236
- active instanceof HTMLInputElement ||
237
- active instanceof HTMLTextAreaElement ||
238
- active instanceof HTMLSelectElement ||
239
- Boolean(active?.closest("[role='textbox']"));
240
- if (formControl && !contentEditable) return;
241
- if (editor?.dataset.slidesEditorEditable === "true" && !contentEditable) {
242
- window.dispatchEvent(new CustomEvent("slides:toggle-layers"));
243
- setCmdkOpen(false);
244
- return;
245
- }
246
- setCmdkOpen(true);
247
- }, []);
226
+ const handleCommandMenuShortcut = useCallback(() => setCmdkOpen(true), []);
248
227
  useCommandMenuShortcut(handleCommandMenuShortcut, {
249
228
  allowContentEditable: true,
250
229
  });
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-27
4
+ ---
5
+
6
+ Image fit controls now apply cover crops and let you choose their position.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-28
4
+ ---
5
+
6
+ A slide containing a `<style>` block no longer renders empty on shared and presented links — the block and everything after it now survive.
package/dist/action.d.ts CHANGED
@@ -19,8 +19,10 @@ import type { ActionAuditConfig } from "./audit/types.js";
19
19
  * selected actions are attributed as `"tool"`.
20
20
  * - `"automation"` — an event-triggered automation dispatched from a stored
21
21
  * workspace trigger with trusted trigger lineage.
22
+ * - `"webmcp"` — a browser agent invoking an automatically projected action
23
+ * through the authenticated page-local WebMCP bridge.
22
24
  */
23
- export type ActionCaller = "tool" | "http" | "frontend" | "cli" | "mcp" | "a2a" | "automation";
25
+ export type ActionCaller = "tool" | "http" | "frontend" | "cli" | "mcp" | "webmcp" | "a2a" | "automation";
24
26
  /**
25
27
  * Trusted automation lineage added by the trigger dispatcher. Action inputs
26
28
  * must never be used to create or override this context.
@@ -1,4 +1,16 @@
1
1
  import { type ReactNode } from "react";
2
+ export interface SubmitFeedbackFormOptions {
3
+ value: string;
4
+ url?: string | null;
5
+ openedAt?: number;
6
+ idempotencyKey?: string | null;
7
+ honeypot?: string;
8
+ submitterEmail?: string | null;
9
+ chatSessionId?: string | null;
10
+ chatStorageKey?: string | null;
11
+ activeRunId?: string | null;
12
+ }
13
+ export declare function submitFeedbackForm(options: SubmitFeedbackFormOptions): Promise<"submitted" | "unconfigured">;
2
14
  export interface FeedbackButtonProps {
3
15
  /**
4
16
  * "sidebar" renders a full-width row with icon + label (for app left sidebars).
@@ -184,6 +184,60 @@ async function loadSchema(target) {
184
184
  schemaCache.set(key, pending);
185
185
  return pending;
186
186
  }
187
+ export async function submitFeedbackForm(options) {
188
+ const resolvedUrl = resolveFeedbackUrl(options.url);
189
+ const target = resolvedUrl ? parseTarget(resolvedUrl) : null;
190
+ if (!target)
191
+ return "unconfigured";
192
+ const value = options.value.trim();
193
+ if (!value)
194
+ throw new Error("Feedback is empty");
195
+ const resolvedSchema = await loadSchema(target);
196
+ const submitterEmail = isSyntheticAgentNativeAnonymousEmail(options.submitterEmail)
197
+ ? null
198
+ : options.submitterEmail;
199
+ const feedbackContext = getFeedbackClientContext({
200
+ chatSessionId: options.chatSessionId,
201
+ storageKey: options.chatStorageKey,
202
+ activeRunId: options.activeRunId,
203
+ });
204
+ const res = await fetch(`${target.endpoint}/api/submit/${encodeURIComponent(resolvedSchema.formId)}`, {
205
+ method: "POST",
206
+ headers: {
207
+ "Content-Type": "application/json",
208
+ ...(options.idempotencyKey
209
+ ? { "Idempotency-Key": options.idempotencyKey }
210
+ : {}),
211
+ },
212
+ body: JSON.stringify({
213
+ data: { [resolvedSchema.fieldId]: value },
214
+ _t: options.openedAt ?? Date.now(),
215
+ _hp: options.honeypot ?? "",
216
+ _meta: {
217
+ ...(submitterEmail ? { submitterEmail } : {}),
218
+ ...feedbackContext,
219
+ },
220
+ }),
221
+ });
222
+ if (!res.ok) {
223
+ const responseBody = await res.text();
224
+ let errorMessage;
225
+ try {
226
+ const body = JSON.parse(responseBody);
227
+ if (body !== null &&
228
+ typeof body === "object" &&
229
+ "error" in body &&
230
+ typeof body.error === "string") {
231
+ errorMessage = body.error.trim() || undefined;
232
+ }
233
+ }
234
+ catch {
235
+ throw new Error(`submit failed (${res.status})`);
236
+ }
237
+ throw new Error(errorMessage || `submit failed (${res.status})`);
238
+ }
239
+ return "submitted";
240
+ }
187
241
  const surfaceStyle = {
188
242
  width: "min(380px, calc(100vw - 32px))",
189
243
  };
@@ -259,7 +313,6 @@ function FeedbackPopoverButton({ variant = "sidebar", label, url, className, sid
259
313
  const [submitting, setSubmitting] = useState(false);
260
314
  const [submitted, setSubmitted] = useState(false);
261
315
  const [error, setError] = useState(null);
262
- const [schema, setSchema] = useState(null);
263
316
  const openedAtRef = useRef(0);
264
317
  const closeTimerRef = useRef(null);
265
318
  const textareaRef = useRef(null);
@@ -273,11 +326,8 @@ function FeedbackPopoverButton({ variant = "sidebar", label, url, className, sid
273
326
  setSubmitting(false);
274
327
  setSubmitted(false);
275
328
  setError(null);
276
- setSchema(null);
277
329
  if (target) {
278
- loadSchema(target)
279
- .then((s) => setSchema(s))
280
- .catch((err) => {
330
+ loadSchema(target).catch((err) => {
281
331
  console.error("[FeedbackButton] schema load failed", err);
282
332
  setError(copy.loadError);
283
333
  });
@@ -306,33 +356,18 @@ function FeedbackPopoverButton({ variant = "sidebar", label, url, className, sid
306
356
  setSubmitting(true);
307
357
  setError(null);
308
358
  try {
309
- const resolvedSchema = schema ?? (await loadSchema(target));
310
- if (!schema)
311
- setSchema(resolvedSchema);
312
359
  const submitterEmail = isSyntheticAgentNativeAnonymousEmail(session?.email)
313
360
  ? null
314
361
  : session?.email;
315
- const feedbackContext = getFeedbackClientContext({
362
+ await submitFeedbackForm({
363
+ url,
364
+ value,
365
+ openedAt: openedAtRef.current,
366
+ honeypot,
367
+ submitterEmail,
316
368
  chatSessionId,
317
- storageKey: chatStorageKey,
369
+ chatStorageKey,
318
370
  });
319
- const res = await fetch(`${target.endpoint}/api/submit/${encodeURIComponent(resolvedSchema.formId)}`, {
320
- method: "POST",
321
- headers: { "Content-Type": "application/json" },
322
- body: JSON.stringify({
323
- data: { [resolvedSchema.fieldId]: trimmed },
324
- _t: openedAtRef.current,
325
- _hp: honeypot,
326
- _meta: {
327
- ...(submitterEmail ? { submitterEmail } : {}),
328
- ...feedbackContext,
329
- },
330
- }),
331
- });
332
- if (!res.ok) {
333
- const body = (await res.json().catch(() => ({})));
334
- throw new Error(body.error || `submit failed (${res.status})`);
335
- }
336
371
  setSubmitted(true);
337
372
  closeTimerRef.current = setTimeout(() => setOpen(false), 1400);
338
373
  }
@@ -342,7 +377,6 @@ function FeedbackPopoverButton({ variant = "sidebar", label, url, className, sid
342
377
  }
343
378
  }, [
344
379
  target,
345
- schema,
346
380
  value,
347
381
  honeypot,
348
382
  submitting,
@@ -66,6 +66,7 @@ import { AgentNativeRouteWarmup } from "./route-warmup.js";
66
66
  import { RouteTransitionIndicator } from "./RouteTransitionIndicator.js";
67
67
  import { RuntimeConfigNotice } from "./RuntimeConfigNotice.js";
68
68
  import { EMBEDDED_THEME_CHANGE_EVENT, applyEmbeddedThemeUpdate, parseEmbeddedThemeUpdate, } from "./theme.js";
69
+ import { createAgentNativeServerActionWebMcpRegistration } from "./webmcp.js";
69
70
  const DEFAULT_TOASTER = (_jsx(Toaster, { richColors: true, position: "bottom-left", offset: { bottom: 44, left: 32 }, mobileOffset: { bottom: 44, left: 16 } }));
70
71
  function RoutedAppEnhancements() {
71
72
  const isInRouter = useInRouterContext();
@@ -73,6 +74,17 @@ function RoutedAppEnhancements() {
73
74
  return null;
74
75
  return (_jsxs(_Fragment, { children: [_jsx(AgentNativeRouteWarmup, {}), _jsx(RouteTransitionIndicator, {})] }));
75
76
  }
77
+ function AutomaticWebMcpActionRegistration() {
78
+ useEffect(() => {
79
+ const registration = createAgentNativeServerActionWebMcpRegistration();
80
+ void registration.start().catch(() => {
81
+ // WebMCP is progressive enhancement. Session expiry or a transient
82
+ // manifest failure must not prevent the authenticated app from loading.
83
+ });
84
+ return () => registration.stop();
85
+ }, []);
86
+ return null;
87
+ }
76
88
  function readDocumentTitleFallback() {
77
89
  const selectors = [
78
90
  'meta[name="application-name"]',
@@ -153,5 +165,5 @@ export function AppProviders({ queryClient, isPublicPath = false, clientOnlyFall
153
165
  if (isPublicPath) {
154
166
  return (_jsx(ProvidersInner, { queryClient: queryClient, defaultTheme: defaultTheme, themeAttribute: themeAttribute, tooltipDelayDuration: tooltipDelayDuration, toaster: toaster, disableThemeTransitions: disableThemeTransitions, i18n: i18n, documentTitleFallback: documentTitleFallback, showProductionEnvironmentBadge: false, children: children }));
155
167
  }
156
- return (_jsx(ClientOnly, { fallback: fallback, children: _jsx(ProvidersInner, { queryClient: queryClient, defaultTheme: defaultTheme, themeAttribute: themeAttribute, tooltipDelayDuration: tooltipDelayDuration, toaster: toaster, disableThemeTransitions: disableThemeTransitions, i18n: i18n, documentTitleFallback: documentTitleFallback, showProductionEnvironmentBadge: !sessionBypass, children: _jsx(RequireSession, { bypass: sessionBypass, fallback: fallback, children: sessionBypass ? (children) : (_jsx(FirstRunOnboardingStartupGate, { children: children })) }) }) }));
168
+ return (_jsx(ClientOnly, { fallback: fallback, children: _jsx(ProvidersInner, { queryClient: queryClient, defaultTheme: defaultTheme, themeAttribute: themeAttribute, tooltipDelayDuration: tooltipDelayDuration, toaster: toaster, disableThemeTransitions: disableThemeTransitions, i18n: i18n, documentTitleFallback: documentTitleFallback, showProductionEnvironmentBadge: !sessionBypass, children: _jsx(RequireSession, { bypass: sessionBypass, fallback: fallback, children: sessionBypass ? (children) : (_jsxs(FirstRunOnboardingStartupGate, { children: [_jsx(AutomaticWebMcpActionRegistration, {}), children] })) }) }) }));
157
169
  }
@@ -9,5 +9,6 @@ export interface FeedbackClientContext {
9
9
  export interface FeedbackClientContextOptions {
10
10
  chatSessionId?: string | null;
11
11
  storageKey?: string | null;
12
+ activeRunId?: string | null;
12
13
  }
13
14
  export declare function getFeedbackClientContext(options?: FeedbackClientContextOptions): FeedbackClientContext;
@@ -64,8 +64,9 @@ export function getFeedbackClientContext(options = {}) {
64
64
  const context = {
65
65
  chatSessionIds: [...ids].slice(0, MAX_CHAT_SESSION_IDS),
66
66
  };
67
- if (activeRun?.runId)
68
- context.activeRunId = activeRun.runId;
67
+ const activeRunId = options.activeRunId === undefined ? activeRun?.runId : options.activeRunId;
68
+ if (activeRunId)
69
+ context.activeRunId = activeRunId;
69
70
  if (typeof window !== "undefined") {
70
71
  context.pageUrl = scrubUrl(window.location.href);
71
72
  context.clientSurface = getClientSurface();
@@ -52,6 +52,7 @@ export interface AgentNativeHostSession {
52
52
  }
53
53
  export interface AgentNativeClientActionRuntime {
54
54
  requestId?: string;
55
+ signal?: AbortSignal;
55
56
  origin: string;
56
57
  context: AgentNativeHostContext;
57
58
  session: AgentNativeHostSession;
@@ -5,20 +5,39 @@ import * as PopoverPrimitive from "@radix-ui/react-popover";
5
5
  import { IconThumbUp, IconThumbDown } from "@tabler/icons-react";
6
6
  import { useState, useCallback, useId, useRef } from "react";
7
7
  import { agentNativePath } from "../api-path.js";
8
+ import { submitFeedbackForm } from "../FeedbackButton.js";
8
9
  import { useT } from "../i18n.js";
10
+ import { useSession } from "../use-session.js";
9
11
  import { cn } from "../utils.js";
12
+ function createFeedbackIdempotencyKey() {
13
+ if (typeof crypto !== "undefined" &&
14
+ typeof crypto.randomUUID === "function") {
15
+ return crypto.randomUUID();
16
+ }
17
+ return `feedback-${Date.now()}-${Math.random().toString(36).slice(2)}`;
18
+ }
10
19
  export function ThumbsFeedback({ threadId, runId, messageSeq, className, }) {
11
20
  const t = useT();
21
+ const { session } = useSession();
12
22
  const [selection, setSelection] = useState(null);
13
23
  const [popoverOpen, setPopoverOpen] = useState(false);
14
24
  const [textFeedback, setTextFeedback] = useState("");
25
+ const [textSubmitting, setTextSubmitting] = useState(false);
15
26
  const feedbackInputId = useId();
16
27
  const feedbackInputRef = useRef(null);
17
- const sendFeedback = useCallback(async (feedbackType, value) => {
28
+ const feedbackOpenedAtRef = useRef(0);
29
+ const textSubmissionInFlightRef = useRef(false);
30
+ const textFeedbackDeliveryRef = useRef(null);
31
+ const sendFeedback = useCallback(async (feedbackType, value, idempotencyKey) => {
18
32
  try {
19
33
  const response = await fetch(agentNativePath("/_agent-native/observability/feedback"), {
20
34
  method: "POST",
21
- headers: { "Content-Type": "application/json" },
35
+ headers: {
36
+ "Content-Type": "application/json",
37
+ ...(feedbackType === "text" && idempotencyKey
38
+ ? { "Idempotency-Key": idempotencyKey }
39
+ : {}),
40
+ },
22
41
  body: JSON.stringify({
23
42
  threadId,
24
43
  runId,
@@ -49,27 +68,71 @@ export function ThumbsFeedback({ threadId, runId, messageSeq, className, }) {
49
68
  }, [selection, sendFeedback]);
50
69
  const handleThumbsDown = useCallback(() => {
51
70
  if (selection === "down") {
52
- setPopoverOpen((prev) => !prev);
71
+ const nextOpen = !popoverOpen;
72
+ if (nextOpen)
73
+ feedbackOpenedAtRef.current = Date.now();
74
+ setPopoverOpen(nextOpen);
53
75
  return;
54
76
  }
77
+ feedbackOpenedAtRef.current = Date.now();
55
78
  setSelection("down");
56
79
  setPopoverOpen(true);
57
80
  void sendFeedback("thumbs_down").then((submitted) => {
58
81
  if (!submitted)
59
82
  setSelection(null);
60
83
  });
61
- }, [selection, sendFeedback]);
84
+ }, [popoverOpen, selection, sendFeedback]);
62
85
  const handleTextFeedback = useCallback(() => {
63
86
  const value = textFeedback.trim();
64
- if (!value)
87
+ if (!value || textSubmissionInFlightRef.current)
65
88
  return;
66
- void sendFeedback("text", value).then((submitted) => {
67
- if (!submitted)
89
+ const delivery = textFeedbackDeliveryRef.current?.value === value
90
+ ? textFeedbackDeliveryRef.current
91
+ : {
92
+ value,
93
+ observabilityDelivered: false,
94
+ sharedFormDelivered: false,
95
+ idempotencyKey: createFeedbackIdempotencyKey(),
96
+ };
97
+ textFeedbackDeliveryRef.current = delivery;
98
+ textSubmissionInFlightRef.current = true;
99
+ setTextSubmitting(true);
100
+ const deliveries = [];
101
+ if (!delivery.observabilityDelivered) {
102
+ deliveries.push(sendFeedback("text", value, delivery.idempotencyKey).then((submitted) => {
103
+ delivery.observabilityDelivered = submitted;
104
+ }));
105
+ }
106
+ if (!delivery.sharedFormDelivered) {
107
+ deliveries.push(submitFeedbackForm({
108
+ value,
109
+ openedAt: feedbackOpenedAtRef.current,
110
+ idempotencyKey: delivery.idempotencyKey,
111
+ chatSessionId: threadId,
112
+ activeRunId: runId,
113
+ submitterEmail: session?.email,
114
+ })
115
+ .then(() => {
116
+ delivery.sharedFormDelivered = true;
117
+ })
118
+ .catch((reason) => {
119
+ console.error("[ThumbsFeedback] shared feedback form submission failed", reason);
120
+ }));
121
+ }
122
+ void Promise.all(deliveries)
123
+ .then(() => {
124
+ if (!delivery.observabilityDelivered || !delivery.sharedFormDelivered) {
68
125
  return;
126
+ }
127
+ textFeedbackDeliveryRef.current = null;
69
128
  setTextFeedback("");
70
129
  setPopoverOpen(false);
130
+ })
131
+ .finally(() => {
132
+ textSubmissionInFlightRef.current = false;
133
+ setTextSubmitting(false);
71
134
  });
72
- }, [sendFeedback, textFeedback]);
135
+ }, [runId, sendFeedback, session?.email, textFeedback, threadId]);
73
136
  return (_jsxs("div", { className: cn("inline-flex items-center gap-0.5", className), children: [_jsx("button", { type: "button", "aria-label": t("agentChat.feedback.thumbsUp"), onClick: handleThumbsUp, className: cn("flex h-6 w-6 items-center justify-center rounded", selection === "up"
74
137
  ? "text-foreground"
75
138
  : "text-muted-foreground/70 hover:text-muted-foreground hover:bg-accent/50"), children: _jsx(IconThumbUp, { size: 16, stroke: selection === "up" ? 2.5 : 1.5, fill: selection === "up" ? "currentColor" : "none" }) }), _jsxs(PopoverPrimitive.Root, { open: popoverOpen, onOpenChange: setPopoverOpen, children: [_jsx(PopoverPrimitive.Trigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": t("agentChat.feedback.thumbsDown"), onClick: handleThumbsDown, className: cn("flex h-6 w-6 items-center justify-center rounded", selection === "down"
@@ -80,7 +143,7 @@ export function ThumbsFeedback({ threadId, runId, messageSeq, className, }) {
80
143
  }, className: "z-[300] w-[min(320px,calc(100vw-24px))] overflow-hidden rounded-lg border border-border bg-popover p-3 text-popover-foreground shadow-lg outline-none origin-[var(--radix-popover-content-transform-origin)]", children: _jsxs("form", { className: "flex flex-col gap-2.5", onSubmit: (event) => {
81
144
  event.preventDefault();
82
145
  handleTextFeedback();
83
- }, children: [_jsx("label", { htmlFor: feedbackInputId, className: "text-xs font-medium text-foreground", children: t("agentChat.feedback.whatWentWrong") }), _jsx(Textarea, { id: feedbackInputId, ref: feedbackInputRef, autoFocus: true, value: textFeedback, onChange: (event) => setTextFeedback(event.target.value), onKeyDown: (event) => {
146
+ }, children: [_jsx("label", { htmlFor: feedbackInputId, className: "text-xs font-medium text-foreground", children: t("agentChat.feedback.whatWentWrong") }), _jsx(Textarea, { id: feedbackInputId, ref: feedbackInputRef, autoFocus: true, value: textFeedback, onChange: (event) => setTextFeedback(event.target.value), disabled: textSubmitting, onKeyDown: (event) => {
84
147
  if ((event.metaKey || event.ctrlKey) &&
85
148
  event.key === "Enter") {
86
149
  event.preventDefault();
@@ -89,5 +152,5 @@ export function ThumbsFeedback({ threadId, runId, messageSeq, className, }) {
89
152
  }, placeholder: t("agentChat.feedback.placeholder"), rows: 3, maxLength: 2000, className: "min-h-20 resize-none px-2.5 py-2 text-xs" }), _jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "text-[11px] text-muted-foreground/75", children: t("agentChat.feedback.keyboardHint").replace("{{shortcut}}", typeof navigator !== "undefined" &&
90
153
  /Mac|iPhone|iPad/.test(navigator.userAgent)
91
154
  ? "⌘"
92
- : "Ctrl") }), _jsx(Button, { type: "submit", size: "sm", disabled: !textFeedback.trim(), className: "h-7 px-2.5 text-xs", children: t("agentChat.feedback.submit") })] })] }) }) })] })] }));
155
+ : "Ctrl") }), _jsx(Button, { type: "submit", size: "sm", disabled: textSubmitting || !textFeedback.trim(), className: "h-7 px-2.5 text-xs", children: t("agentChat.feedback.submit") })] })] }) }) })] })] }));
93
156
  }
@@ -71,4 +71,8 @@ export interface AgentNativeWebMcpRegistration {
71
71
  start(): Promise<void>;
72
72
  stop(): void;
73
73
  }
74
+ export declare function createAgentNativeServerActionWebMcpRegistration(options?: {
75
+ document?: Document;
76
+ fetch?: typeof fetch;
77
+ }): AgentNativeWebMcpRegistration;
74
78
  export declare function createAgentNativeWebMcpRegistration(options: AgentNativeWebMcpRegistrationOptions): AgentNativeWebMcpRegistration;
@@ -234,6 +234,52 @@ export function createAgentNativeWebMcpClient(options = {}) {
234
234
  };
235
235
  return client;
236
236
  }
237
+ export function createAgentNativeServerActionWebMcpRegistration(options) {
238
+ const fetchImpl = options?.fetch ?? ((...args) => fetch(...args));
239
+ return createAgentNativeWebMcpRegistration({
240
+ document: options?.document,
241
+ maxToolCount: 1_000,
242
+ maxDescriptionChars: 10_000,
243
+ actions: async () => {
244
+ const response = await fetchImpl("/_agent-native/webmcp/manifest", {
245
+ credentials: "same-origin",
246
+ headers: { Accept: "application/json" },
247
+ });
248
+ if (!response.ok) {
249
+ throw new Error(`Unable to load WebMCP actions (${response.status})`);
250
+ }
251
+ const manifest = (await response.json());
252
+ if (!Array.isArray(manifest)) {
253
+ throw new Error("WebMCP action manifest must be an array");
254
+ }
255
+ return manifest.map((action) => ({
256
+ name: action.name,
257
+ description: action.description,
258
+ ...(action.inputSchema ? { schema: action.inputSchema } : {}),
259
+ ...(action.readOnly ? { readOnly: true } : {}),
260
+ run: async (args, runtime) => {
261
+ const result = await fetchImpl(`/_agent-native/webmcp/actions/${encodeURIComponent(action.name)}`, {
262
+ method: "POST",
263
+ credentials: "same-origin",
264
+ headers: {
265
+ Accept: "application/json",
266
+ "Content-Type": "application/json",
267
+ },
268
+ body: JSON.stringify(args),
269
+ ...(runtime.signal ? { signal: runtime.signal } : {}),
270
+ });
271
+ const body = await result.json();
272
+ if (!result.ok) {
273
+ throw new Error(isRecord(body) && typeof body.error === "string"
274
+ ? body.error
275
+ : `WebMCP action failed (${result.status})`);
276
+ }
277
+ return body;
278
+ },
279
+ }));
280
+ },
281
+ });
282
+ }
237
283
  function resolveActions(actions) {
238
284
  return Promise.resolve(typeof actions === "function" ? actions() : actions).then((value) => {
239
285
  if (!Array.isArray(value)) {
@@ -271,7 +317,7 @@ function actionManifest(action) {
271
317
  function actionInputSchema(action) {
272
318
  return action.schema ?? action.parameters ?? EMPTY_INPUT_SCHEMA;
273
319
  }
274
- function actionRuntime(options, context, session) {
320
+ function actionRuntime(options, context, session, signal) {
275
321
  const origin = options.origin ?? "agent-native-webmcp";
276
322
  const runCommand = async (command, payload) => {
277
323
  const handler = options.commands?.[command] ??
@@ -281,6 +327,7 @@ function actionRuntime(options, context, session) {
281
327
  return handler({ command, payload, origin }, undefined);
282
328
  };
283
329
  return {
330
+ ...(signal ? { signal } : {}),
284
331
  origin,
285
332
  context,
286
333
  session,
@@ -381,7 +428,7 @@ export function createAgentNativeWebMcpRegistration(options) {
381
428
  : {}),
382
429
  },
383
430
  execute: async (input, executionOptions) => {
384
- if (executionOptions.signal.aborted) {
431
+ if (executionOptions?.signal.aborted) {
385
432
  throw new Error(`WebMCP action "${action.name}" was aborted`);
386
433
  }
387
434
  validateBoundedJson(input, `WebMCP action "${action.name}" input`, options.maxInputChars ?? DEFAULT_INPUT_CHARS);
@@ -417,8 +464,8 @@ export function createAgentNativeWebMcpRegistration(options) {
417
464
  throw new Error(`WebMCP action "${action.name}" was not approved`);
418
465
  }
419
466
  }
420
- const result = await action.run(input, actionRuntime(options, context, session));
421
- if (executionOptions.signal.aborted) {
467
+ const result = await action.run(input, actionRuntime(options, context, session, executionOptions?.signal));
468
+ if (executionOptions?.signal.aborted) {
422
469
  throw new Error(`WebMCP action "${action.name}" was aborted`);
423
470
  }
424
471
  return serializeWebMcpResult(result, `WebMCP action "${action.name}" result`, options.maxResultChars ?? DEFAULT_RESULT_CHARS);
@@ -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;
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- ok?: undefined;
17
16
  error: string;
17
+ ok?: undefined;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;