@agent-native/core 0.161.7 → 0.161.9

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 (65) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/content/server/plugins/agent-chat.ts +2 -0
  3. package/corpus/templates/content/server/plugins/feature-flags.ts +5 -0
  4. package/corpus/templates/content/shared/feature-flags.ts +15 -0
  5. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +180 -37
  6. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -3
  7. package/corpus/templates/design/app/components/layout/Layout.tsx +4 -1
  8. package/corpus/templates/design/app/hooks/use-navigation-state.ts +13 -2
  9. package/corpus/templates/design/app/i18n-data.ts +11 -0
  10. package/corpus/templates/design/app/lib/agent-chat.ts +30 -0
  11. package/corpus/templates/design/app/lib/builder-host-chat.ts +34 -0
  12. package/corpus/templates/design/app/lib/builder-host-origin.ts +31 -0
  13. package/corpus/templates/design/app/lib/embed-chrome.ts +70 -0
  14. package/corpus/templates/design/app/lib/shell-design.ts +113 -0
  15. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +89 -0
  16. package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +85 -12
  17. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +43 -19
  18. package/corpus/templates/design/app/pages/design-editor/screen-command-utils.ts +9 -2
  19. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +13 -0
  20. package/corpus/templates/design/app/root.tsx +23 -1
  21. package/corpus/templates/design/server/lib/fusion-screens.ts +17 -1
  22. package/corpus/templates/design/server/plugins/builder-host-embed-headers.ts +37 -0
  23. package/corpus/templates/design/server/routes/[...page].get.ts +1 -0
  24. package/corpus/templates/design/shared/builder-preview-url.ts +113 -0
  25. package/corpus/templates/design/shared/full-app.ts +19 -0
  26. package/corpus/templates/design/shared/shell-screens.ts +139 -0
  27. package/corpus/templates/design/shared/source-mode.ts +10 -0
  28. package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +9 -0
  29. package/corpus/templates/slides/actions/patch-deck.ts +8 -0
  30. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +188 -66
  31. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +102 -22
  32. package/corpus/templates/slides/app/components/presentation/PresenterView.tsx +84 -2
  33. package/corpus/templates/slides/app/context/DeckContext.tsx +94 -3
  34. package/corpus/templates/slides/app/i18n/en-US.ts +5 -0
  35. package/corpus/templates/slides/app/pages/DeckEditor.tsx +122 -8
  36. package/corpus/templates/videos/package.json +9 -0
  37. package/dist/a2a/correlation.d.ts +6 -6
  38. package/dist/a2a/correlation.js +8 -6
  39. package/dist/a2a/types.d.ts +7 -5
  40. package/dist/client/RuntimeConfigNotice.js +3 -0
  41. package/dist/client/api-surface.d.ts +19 -0
  42. package/dist/client/api-surface.js +32 -0
  43. package/dist/client/application-state.js +4 -0
  44. package/dist/client/builder-frame.d.ts +6 -0
  45. package/dist/client/builder-frame.js +1 -1
  46. package/dist/client/client-status-requests.js +5 -0
  47. package/dist/client/host/index.d.ts +1 -0
  48. package/dist/client/host/index.js +1 -0
  49. package/dist/client/use-action.d.ts +1 -1
  50. package/dist/client/use-action.js +17 -0
  51. package/dist/client/use-session.js +5 -0
  52. package/dist/collab/struct-routes.d.ts +1 -1
  53. package/dist/observability/routes.d.ts +5 -5
  54. package/dist/progress/routes.d.ts +1 -1
  55. package/dist/provider-api/actions/provider-api.d.ts +6 -6
  56. package/dist/scripts/call-agent.js +6 -5
  57. package/dist/secrets/routes.d.ts +9 -9
  58. package/dist/server/agent-chat/action-filters-a2a.d.ts +6 -1
  59. package/dist/server/agent-chat/action-filters-a2a.js +29 -2
  60. package/dist/server/agent-chat/plugin-options.d.ts +6 -0
  61. package/dist/server/agent-chat-plugin.d.ts +2 -1
  62. package/dist/server/agent-chat-plugin.js +20 -4
  63. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  64. package/package.json +1 -1
  65. /package/corpus/templates/design/app/routes/{visual-edit.$id.tsx → visual-edit_.$id.tsx} +0 -0
@@ -32,6 +32,8 @@ import {
32
32
  import { getDb, schema } from "../db/index.js";
33
33
  import { mutateDesignData } from "./design-data-mutation.js";
34
34
 
35
+ const PATH_BASE_PLACEHOLDER = "http://fusion-screen-base.invalid";
36
+
35
37
  /** Default iframe viewport, mirroring add-localhost-screens' defaults. */
36
38
  export const DEFAULT_FUSION_SCREEN_WIDTH = 1280;
37
39
  export const DEFAULT_FUSION_SCREEN_HEIGHT = 900;
@@ -129,9 +131,23 @@ export async function upsertFusionScreens(args: {
129
131
 
130
132
  const results: FusionScreenResult[] = [];
131
133
 
134
+ // The base may carry a path prefix (the builder-host preview proxy), so route
135
+ // paths join onto it rather than resolve against it: `new URL("/", base)`
136
+ // would drop the prefix and point at the origin root.
137
+ const originRelativeBase = previewUrl.startsWith("/");
138
+ const baseWithSlash = previewUrl.endsWith("/")
139
+ ? previewUrl
140
+ : `${previewUrl}/`;
141
+ const resolutionBase = originRelativeBase
142
+ ? new URL(baseWithSlash, PATH_BASE_PLACEHOLDER).toString()
143
+ : baseWithSlash;
144
+
132
145
  for (let index = 0; index < paths.length; index += 1) {
133
146
  const path = paths[index]!;
134
- const url = new URL(path, previewUrl).toString();
147
+ const screenUrl = new URL(path.replace(/^\/+/, ""), resolutionBase);
148
+ const url = originRelativeBase
149
+ ? `${screenUrl.pathname}${screenUrl.search}`
150
+ : screenUrl.toString();
135
151
  const preferredFilename = `fusion-${slugForPath(path)}.html`;
136
152
  const existing = existingByFilename.get(preferredFilename);
137
153
  const filename = existing?.filename ?? uniqueFilename(path, usedFilenames);
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `require-corp` would make the browser reject the containers this canvas frames,
3
+ * and they cannot opt in via CORP. `frame-ancestors` replaces the embed token as
4
+ * the limit on who may embed this route. A `response` hook, not middleware: core
5
+ * sets its security headers from a plugin, which runs later.
6
+ */
7
+ import { getRequestURL, type H3Event } from "h3";
8
+
9
+ import { SHELL_CANVAS_PATH } from "../../shared/shell-screens.js";
10
+
11
+ const BUILDER_FRAME_ANCESTORS = [
12
+ "https://builder.io",
13
+ "https://*.builder.io",
14
+ "https://*.builder.my",
15
+ "http://localhost:*",
16
+ "http://127.0.0.1:*",
17
+ ].join(" ");
18
+
19
+ export function isShellCanvasRequest(event: H3Event): boolean {
20
+ try {
21
+ return getRequestURL(event).pathname === SHELL_CANVAS_PATH;
22
+ // coercion-ok: an unparseable URL is not the shell route.
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ export default (nitroApp: any): void => {
29
+ nitroApp.hooks.hook("response", (res: Response, event: H3Event) => {
30
+ if (!isShellCanvasRequest(event)) return;
31
+ res.headers.set("Cross-Origin-Embedder-Policy", "unsafe-none");
32
+ res.headers.set(
33
+ "Content-Security-Policy",
34
+ `frame-ancestors ${BUILDER_FRAME_ANCESTORS}`,
35
+ );
36
+ });
37
+ };
@@ -10,6 +10,7 @@ import {
10
10
  } from "@agent-native/core/shared";
11
11
  import {
12
12
  defineEventHandler,
13
+ getHeader,
13
14
  getQuery,
14
15
  getRequestURL,
15
16
  setResponseHeader,
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Validation for Builder Fusion container preview URLs. Stops the seam becoming
3
+ * an open embed primitive pointed at an internal address. Mirrors
4
+ * builder-internal's list (`packages/app/models/fusion.model.tsx` —
5
+ * search `builderio.xyz`); keep the two in sync.
6
+ */
7
+
8
+ const BUILDER_PREVIEW_HOST_SUFFIXES = [
9
+ ".fly.dev",
10
+ ".builderio.xyz",
11
+ ".builderio.dev",
12
+ ".builder.codes",
13
+ ".builder.my",
14
+ ".builder.live",
15
+ ] as const;
16
+
17
+ const LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
18
+
19
+ /**
20
+ * Development only, and fail closed: the proxy fetches this URL from the
21
+ * server, so in production a loopback host means scanning the Design host
22
+ * itself on a caller's behalf.
23
+ */
24
+ export function isLoopbackPreviewAllowed(): boolean {
25
+ const nodeEnv =
26
+ typeof process === "undefined" ? undefined : process.env?.NODE_ENV;
27
+ // Wins over the bundler flag: a dev-mode bundle served by a production
28
+ // process is still production.
29
+ if (nodeEnv === "production") return false;
30
+ if (nodeEnv === "development" || nodeEnv === "test") return true;
31
+ const viteEnv = (import.meta as { env?: { DEV?: boolean } }).env;
32
+ return viteEnv?.DEV === true;
33
+ }
34
+
35
+ export class InvalidBuilderPreviewUrlError extends Error {
36
+ constructor(reason: string) {
37
+ super(`Invalid Builder preview URL: ${reason}`);
38
+ this.name = "InvalidBuilderPreviewUrlError";
39
+ }
40
+ }
41
+
42
+ function isLoopbackHostname(hostname: string): boolean {
43
+ return LOOPBACK_HOSTNAMES.has(hostname);
44
+ }
45
+
46
+ /**
47
+ * Parse and validate a preview URL, returning its normalized form. Throws
48
+ * rather than returning null so a rejected URL is never indistinguishable from
49
+ * an absent one — a bad init must not quietly place zero screens and read as an
50
+ * empty design.
51
+ */
52
+ export function parseBuilderPreviewUrl(raw: unknown): URL {
53
+ if (typeof raw !== "string" || !raw.trim()) {
54
+ throw new InvalidBuilderPreviewUrlError("must be a non-empty string");
55
+ }
56
+
57
+ let url: URL;
58
+ try {
59
+ url = new URL(raw.trim());
60
+ } catch {
61
+ throw new InvalidBuilderPreviewUrlError(`could not parse "${raw}"`);
62
+ }
63
+
64
+ // Credentials would be replayed by the iframe on every request.
65
+ if (url.username || url.password) {
66
+ throw new InvalidBuilderPreviewUrlError("must not embed credentials");
67
+ }
68
+
69
+ const hostname = url.hostname.toLowerCase();
70
+ if (isLoopbackHostname(hostname) && !isLoopbackPreviewAllowed()) {
71
+ throw new InvalidBuilderPreviewUrlError(
72
+ "loopback hosts are only allowed in development",
73
+ );
74
+ }
75
+ const loopback = isLoopbackHostname(hostname);
76
+
77
+ if (url.protocol !== "https:" && !(loopback && url.protocol === "http:")) {
78
+ throw new InvalidBuilderPreviewUrlError(
79
+ `must use https (got "${url.protocol}")`,
80
+ );
81
+ }
82
+
83
+ if (
84
+ !loopback &&
85
+ !BUILDER_PREVIEW_HOST_SUFFIXES.some((suffix) => hostname.endsWith(suffix))
86
+ ) {
87
+ throw new InvalidBuilderPreviewUrlError(
88
+ `host "${hostname}" is not a recognized Builder preview host`,
89
+ );
90
+ }
91
+
92
+ return url;
93
+ }
94
+
95
+ /** Non-throwing form, for UI that wants to branch instead of failing. */
96
+ /**
97
+ * Origin only. `interactiveFrameUrl` carries whatever route the user is
98
+ * previewing, and resolving screen paths against that base nests them under it
99
+ * (`/app.html` + `/about` → `/app.html/about`).
100
+ */
101
+ export function builderPreviewOrigin(raw: unknown): string {
102
+ return parseBuilderPreviewUrl(raw).origin;
103
+ }
104
+
105
+ export function isBuilderPreviewUrl(raw: unknown): boolean {
106
+ try {
107
+ parseBuilderPreviewUrl(raw);
108
+ return true;
109
+ // coercion-ok: "does not parse" is what this predicate reports as false.
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
@@ -29,11 +29,23 @@ export const FULL_APP_BUILDING = defineFeatureFlag({
29
29
 
30
30
  export type DesignFusionAppStatus = "building" | "ready" | "error";
31
31
 
32
+ /**
33
+ * How the linkage was established. `builder-host` (opened from builder.io's
34
+ * embedded Design tab) never provisions or deploys the container — Builder owns
35
+ * it, and Builder's chat session drives it. Absent means `design-app`.
36
+ */
37
+ export type DesignFusionAppSource = "design-app" | "builder-host";
38
+
32
39
  export interface DesignFusionApp {
33
40
  /** Builder project id the app branch lives in. */
34
41
  projectId: string;
35
42
  /** Branch backing this design (one branch per design). */
36
43
  branchName: string;
44
+ source?: DesignFusionAppSource;
45
+ /** Builder organization owning the project. Set for `builder-host` designs. */
46
+ builderOrgId?: string;
47
+ /** Builder content id the tab was opened from, when there is one. */
48
+ contentId?: string;
37
49
  /** Builder visual-editor URL for the branch (progress/debugging). */
38
50
  editorUrl?: string;
39
51
  /** Container dev-server URL once the container is ready; iframe-able. */
@@ -94,10 +106,17 @@ export function readFusionApp(data: unknown): DesignFusionApp | null {
94
106
  app.status === "ready" || app.status === "error" ? app.status : "building";
95
107
  const str = (value: unknown): string | undefined =>
96
108
  typeof value === "string" && value ? value : undefined;
109
+ const source: DesignFusionAppSource | undefined =
110
+ app.source === "builder-host" || app.source === "design-app"
111
+ ? app.source
112
+ : undefined;
97
113
  return {
98
114
  projectId,
99
115
  branchName,
100
116
  status,
117
+ source,
118
+ builderOrgId: str(app.builderOrgId),
119
+ contentId: str(app.contentId),
101
120
  editorUrl: str(app.editorUrl),
102
121
  previewUrl: str(app.previewUrl),
103
122
  statusMessage: str(app.statusMessage),
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Builds the same `{ screens, placedFrames }` shape as `upsertFusionScreens`,
3
+ * but in memory and without a database, for a canvas the host drives entirely
4
+ * over postMessage. Keep the derivations here identical to the server version:
5
+ * a screen that changes shape between the two paths changes fileIds, and the
6
+ * canvas keys selection and frame geometry on those.
7
+ */
8
+
9
+ import type { CanvasFramePlacement } from "./canvas-frames.js";
10
+
11
+ /** The host-driven canvas route: no design row, no session, no server writes. */
12
+ export const SHELL_CANVAS_PATH = "/visual-edit/shell";
13
+
14
+ /** Mirrors add-localhost-screens' defaults, as the server builder does. */
15
+ export const DEFAULT_SHELL_SCREEN_WIDTH = 1280;
16
+ export const DEFAULT_SHELL_SCREEN_HEIGHT = 900;
17
+ const DEFAULT_SHELL_GAP = 160;
18
+ export const MAX_SHELL_SCREENS = 100;
19
+
20
+ export interface ShellScreen {
21
+ fileId: string;
22
+ filename: string;
23
+ path: string;
24
+ url: string;
25
+ title: string;
26
+ width: number;
27
+ height: number;
28
+ }
29
+
30
+ export interface ShellScreensResult {
31
+ screens: ShellScreen[];
32
+ placedFrames: Array<{
33
+ fileId: string;
34
+ filename?: string;
35
+ frame: CanvasFramePlacement;
36
+ }>;
37
+ }
38
+
39
+ function slugForPath(path: string): string {
40
+ const slug = path
41
+ .replace(/^\/+/, "")
42
+ .replace(/[^a-zA-Z0-9]+/g, "-")
43
+ .replace(/^-+|-+$/g, "")
44
+ .toLowerCase();
45
+ return (slug || "home").slice(0, 80);
46
+ }
47
+
48
+ function titleFromPath(path: string): string {
49
+ const trimmed = path.replace(/^\/+|\/+$/g, "");
50
+ if (!trimmed) return "Home";
51
+ const last = trimmed.split("/").pop() ?? trimmed;
52
+ return last.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
53
+ }
54
+
55
+ function uniqueFilename(path: string, used: Set<string>): string {
56
+ const base = `fusion-${slugForPath(path)}.html`;
57
+ let filename = base;
58
+ let suffix = 2;
59
+ while (used.has(filename)) {
60
+ filename = `${base.replace(/\.html$/, "")}-${suffix}.html`;
61
+ suffix += 1;
62
+ }
63
+ used.add(filename);
64
+ return filename;
65
+ }
66
+
67
+ /**
68
+ * Derived from the filename rather than random: a remount must produce the same
69
+ * ids or the canvas loses selection and frame geometry on every reload.
70
+ */
71
+ function shellFileId(filename: string): string {
72
+ return `shell-${filename.replace(/\.html$/, "")}`;
73
+ }
74
+
75
+ export function buildShellScreens(args: {
76
+ previewOrigin: string;
77
+ paths: string[];
78
+ width?: number;
79
+ height?: number;
80
+ startX?: number;
81
+ startY?: number;
82
+ gap?: number;
83
+ }): ShellScreensResult {
84
+ const {
85
+ previewOrigin,
86
+ paths,
87
+ width = DEFAULT_SHELL_SCREEN_WIDTH,
88
+ height = DEFAULT_SHELL_SCREEN_HEIGHT,
89
+ startX = 0,
90
+ startY = 0,
91
+ gap = DEFAULT_SHELL_GAP,
92
+ } = args;
93
+
94
+ const baseWithSlash = previewOrigin.endsWith("/")
95
+ ? previewOrigin
96
+ : `${previewOrigin}/`;
97
+ const baseOrigin = new URL(baseWithSlash).origin;
98
+
99
+ const used = new Set<string>();
100
+ const screens: ShellScreen[] = [];
101
+ const placedFrames: ShellScreensResult["placedFrames"] = [];
102
+ const seenPaths = new Set<string>();
103
+ let nextX = startX;
104
+
105
+ // The host assembles this list from a repo parse plus visited URLs, so a bad
106
+ // parse must not mount hundreds of live iframes. Well above any real route
107
+ // count, so it only ever trips on pathological input.
108
+ for (const rawPath of paths.slice(0, MAX_SHELL_SCREENS)) {
109
+ const path = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
110
+ // A duplicate path would otherwise get a second frame stacked on the first.
111
+ if (seenPaths.has(path)) continue;
112
+ seenPaths.add(path);
113
+
114
+ // `\\host` survives the leading-slash strip and resolves protocol-relative,
115
+ // so a route from the host could otherwise place a frame on another origin.
116
+ const resolved = new URL(path.replace(/^\/+/, ""), baseWithSlash);
117
+ if (resolved.origin !== baseOrigin) continue;
118
+
119
+ const filename = uniqueFilename(path, used);
120
+ const fileId = shellFileId(filename);
121
+ screens.push({
122
+ fileId,
123
+ filename,
124
+ path,
125
+ url: resolved.toString(),
126
+ title: titleFromPath(path),
127
+ width,
128
+ height,
129
+ });
130
+ placedFrames.push({
131
+ fileId,
132
+ filename,
133
+ frame: { fileId, filename, x: nextX, y: startY, width, height },
134
+ });
135
+ nextX += width + gap;
136
+ }
137
+
138
+ return { screens, placedFrames };
139
+ }
@@ -420,6 +420,16 @@ export function isDesignSourceType(value: unknown): value is DesignSourceType {
420
420
  );
421
421
  }
422
422
 
423
+ /**
424
+ * Screens whose content is a URL into a running app. Their markup lives in the
425
+ * app's source, so every edit is a handoff, never a design-file write — the
426
+ * distinction the editor keeps having to make, and keeps making per-call-site.
427
+ */
428
+ export function isRunningAppSourceType(value: unknown): boolean {
429
+ const sourceType = normalizeDesignSourceType(value);
430
+ return sourceType === "localhost" || sourceType === "fusion";
431
+ }
432
+
423
433
  export function normalizeDesignSourceType(
424
434
  value: unknown,
425
435
  ): DesignSourceType | null {
@@ -90,6 +90,15 @@ To edit a slide's content:
90
90
  preserve quote, speaker, date, metric, and uncertainty status. Existing HTML
91
91
  or visual similarity is not proof of source fidelity.
92
92
 
93
+ ## Skipping a Slide
94
+
95
+ Set a slide's `skipped: true` via a `patch-deck` `patch-slide` operation to
96
+ exclude it from Present/Presenter playback without deleting it — the slide
97
+ stays in the deck, editor, and exports. Set `skipped: false` (or omit it) to
98
+ include it again. The rail's right-click menu on each slide thumbnail offers
99
+ Cut, Copy, Paste, Delete, New slide, Duplicate slide, and Skip slide as the
100
+ same operations.
101
+
93
102
  ## Click-to-reveal animations
94
103
 
95
104
  Animations are metadata over the final slide HTML, not alternate slide markup.
@@ -113,6 +113,12 @@ const SlideFieldsSchema = z.object({
113
113
  .describe(
114
114
  "Complete ordered on-click reveal list. Include every intended target in order; unlisted elements remain visible. Use elementPath from the final HTML and 0-based indexes.",
115
115
  ),
116
+ skipped: z
117
+ .boolean()
118
+ .optional()
119
+ .describe(
120
+ "Exclude this slide from Present/Presenter playback without deleting it.",
121
+ ),
116
122
  });
117
123
 
118
124
  /** Update fields on a single existing slide */
@@ -165,6 +171,7 @@ const AddSlideOp = z.object({
165
171
  .optional(),
166
172
  animations: z.array(z.unknown()).optional(),
167
173
  splitByParagraph: z.boolean().optional(),
174
+ skipped: z.boolean().optional(),
168
175
  })
169
176
  .passthrough(),
170
177
  });
@@ -346,6 +353,7 @@ export function applyOperation(deck: any, op: Operation): void {
346
353
  slide.excalidrawData = fields.excalidrawData;
347
354
  if (fields.transition !== undefined) slide.transition = fields.transition;
348
355
  if (fields.animations !== undefined) slide.animations = fields.animations;
356
+ if (fields.skipped !== undefined) slide.skipped = fields.skipped;
349
357
  break;
350
358
  }
351
359