@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/actions/set-workspace-feature-flag.ts +2 -2
- package/corpus/templates/analytics/app/components/agents/FeatureFlagsFleetPanel.tsx +16 -1
- package/corpus/templates/analytics/changelog/2026-08-27-show-analytics-s-own-registered-flags-in-fleet-feature-flag-.md +6 -0
- package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +204 -32
- package/corpus/templates/assets/changelog/2026-08-26-templates-across-brand-kits.md +1 -0
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +61 -68
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +9 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +3 -3
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +8 -3
- package/corpus/templates/calendar/app/lib/calendar-timezone.ts +12 -3
- 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
- package/corpus/templates/calendar/changelog/2026-08-28-event-creators-can-choose-whether-a-meeting-shows-as-free-or.md +6 -0
- package/corpus/templates/clips/app/global.css +0 -27
- package/corpus/templates/clips/app/i18n/en-US.ts +2 -0
- package/corpus/templates/clips/app/routes/download.tsx +131 -16
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +35 -20
- package/corpus/templates/clips/server/routes/api/clips-latest.json.get.ts +77 -22
- package/corpus/templates/forms/actions/response-insights.ts +4 -4
- package/corpus/templates/forms/actions/retry-response-deliveries.ts +26 -0
- package/corpus/templates/forms/server/db/schema.ts +64 -17
- package/corpus/templates/forms/server/handlers/submissions.ts +1022 -94
- package/corpus/templates/forms/server/lib/integrations.ts +100 -33
- package/corpus/templates/forms/server/middleware/00-public-cors.ts +1 -1
- package/corpus/templates/forms/server/plugins/db.ts +40 -0
- package/corpus/templates/plan/agent-native.json +1 -4
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +6 -1
- package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +2 -2
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -6
- package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +78 -12
- package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +4 -4
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +111 -27
- package/corpus/templates/slides/app/components/editor/SlidesLayersPanel.tsx +20 -34
- package/corpus/templates/slides/app/components/editor/slide-style.ts +2 -0
- package/corpus/templates/slides/app/global.css +35 -0
- package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +133 -5
- package/corpus/templates/slides/app/lib/slide-image-replacement.ts +675 -0
- package/corpus/templates/slides/app/lib/validate-native-slide-html.ts +1 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +44 -44
- package/corpus/templates/slides/app/root.tsx +1 -22
- package/corpus/templates/slides/changelog/2026-08-27-image-fit-controls-apply-cover-crops-and-position.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-28-shared-slides-with-a-style-block-render-again.md +6 -0
- package/dist/action.d.ts +3 -1
- package/dist/client/FeedbackButton.d.ts +12 -0
- package/dist/client/FeedbackButton.js +62 -28
- package/dist/client/app-providers.js +13 -1
- package/dist/client/feedback-context.d.ts +1 -0
- package/dist/client/feedback-context.js +3 -2
- package/dist/client/host-bridge.d.ts +1 -0
- package/dist/client/observability/ThumbsFeedback.js +73 -10
- package/dist/client/webmcp.d.ts +4 -0
- package/dist/client/webmcp.js +51 -4
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/index.browser.d.ts +1 -1
- package/dist/index.browser.js +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/observability/routes.js +8 -2
- package/dist/observability/store.d.ts +1 -1
- package/dist/observability/store.js +18 -3
- package/dist/observability/types.d.ts +1 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-routes.d.ts +1 -6
- package/dist/server/action-routes.js +58 -17
- package/dist/server/agent-chat-plugin.js +8 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/webmcp.mdx +31 -12
- 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:
|
|
1054
|
-
|
|
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
|
-
[
|
|
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
|
});
|
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
|
-
|
|
362
|
+
await submitFeedbackForm({
|
|
363
|
+
url,
|
|
364
|
+
value,
|
|
365
|
+
openedAt: openedAtRef.current,
|
|
366
|
+
honeypot,
|
|
367
|
+
submitterEmail,
|
|
316
368
|
chatSessionId,
|
|
317
|
-
|
|
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) : (
|
|
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
|
-
|
|
68
|
-
|
|
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();
|
|
@@ -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
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
}
|
package/dist/client/webmcp.d.ts
CHANGED
|
@@ -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;
|
package/dist/client/webmcp.js
CHANGED
|
@@ -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
|
|
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
|
|
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);
|
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;
|
|
@@ -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;
|