@elytracms/studio 0.0.84 → 0.0.86
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/convex/delivery.ts +21 -0
- package/convex/engine-version.test.ts +31 -0
- package/dist/components/ai/ai-chat-panel.js +13 -2
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +5 -4
- package/dist/components/builder/blocks-model.js +5 -4
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/clipboard-actions.d.ts +6 -0
- package/dist/components/builder/clipboard-actions.js +15 -1
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +13 -0
- package/dist/components/builder/composition-field-editor.js +19 -4
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/insert-context.js +6 -4
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +7 -0
- package/dist/components/cms/cell-edit.js +9 -1
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/collection-table.js +11 -0
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/document-url-path.d.ts +7 -4
- package/dist/components/cms/document-url-path.js +15 -1
- package/dist/components/cms/document-url-path.js.map +1 -1
- package/dist/components/cms/field-control.js +4 -0
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/inline-field-errors.d.ts +36 -0
- package/dist/components/cms/inline-field-errors.js +13 -5
- package/dist/components/cms/inline-field-errors.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +6 -3
- package/dist/components/cms/rich-text/insert.js +9 -6
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- package/dist/components/cms/use-live-url.d.ts +18 -6
- package/dist/components/cms/use-live-url.js +10 -0
- package/dist/components/cms/use-live-url.js.map +1 -1
- package/dist/components/components/transfer.d.ts +6 -5
- package/dist/components/components/transfer.js +7 -6
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/deployment-setup-gate.js +9 -3
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/lib/ai/constraints.js +54 -71
- package/dist/lib/ai/constraints.js.map +1 -1
- package/dist/lib/ai/index.d.ts +1 -0
- package/dist/lib/ai/index.js +1 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/route-fetch.d.ts +58 -0
- package/dist/lib/ai/route-fetch.js +98 -0
- package/dist/lib/ai/route-fetch.js.map +1 -0
- package/dist/lib/ai/route-provider.d.ts +5 -22
- package/dist/lib/ai/route-provider.js +23 -1
- package/dist/lib/ai/route-provider.js.map +1 -1
- package/dist/lib/clipboard/paste-target.d.ts +23 -1
- package/dist/lib/clipboard/paste-target.js +12 -11
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms/document-gate.d.ts +19 -9
- package/dist/lib/cms/document-gate.js +25 -4
- package/dist/lib/cms/document-gate.js.map +1 -1
- package/dist/lib/engine-version.d.ts +16 -0
- package/dist/lib/engine-version.js +17 -0
- package/dist/lib/engine-version.js.map +1 -0
- package/dist/lib/i18n/messages.de.js +4 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +4 -0
- package/dist/lib/i18n/messages.en.js +7 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/studio-operations.js +4 -2
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/deployment-readiness.d.ts +8 -1
- package/dist/lib/persistence/deployment-readiness.js +4 -0
- package/dist/lib/persistence/deployment-readiness.js.map +1 -1
- package/dist/previews/manifest-format.d.ts +7 -7
- package/dist/previews/manifest-format.js +0 -21
- package/dist/previews/manifest-format.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AI route's response-shape guard (EC-514 rider): ONE wrapping `fetch` shared by
|
|
3
|
+
* both client call sites of the mounted route — the panel's chat transport
|
|
4
|
+
* (`useChat` + `DefaultChatTransport`, which accepts a `fetch` option) and the
|
|
5
|
+
* structured proxy in `createRouteProvider` — so "the route is not mounted at this
|
|
6
|
+
* path" is a typed, nameable failure at both, instead of whatever each transport
|
|
7
|
+
* makes of a page of HTML.
|
|
8
|
+
*
|
|
9
|
+
* Why this exists (measured, EC-514): with the EC-503 dev degrade active (studio
|
|
10
|
+
* `dist` missing → the Vite dev mount is skipped) a `POST /api/ai` falls through to
|
|
11
|
+
* the SPA shell — `200 text/html`. The chat transport treats a 200 as success and
|
|
12
|
+
* parses the HTML as an event stream; the HTML carries no `data:` lines, so the
|
|
13
|
+
* stream ends EMPTY and the panel shows the user's turn followed by nothing — no
|
|
14
|
+
* error note, no assistant turn. A silent failure, the exact state AGENTS.md §4
|
|
15
|
+
* forbids. The structured proxy's path is no better by construction: a 200 is
|
|
16
|
+
* `response.ok`, so it calls `response.json()` on `<!DOCTYPE …` — a parse error
|
|
17
|
+
* that names neither the path nor the cause.
|
|
18
|
+
*
|
|
19
|
+
* The contract this guard enforces is the route's OWN: `createAiRouteHandler` only
|
|
20
|
+
* ever answers `application/json` (every non-stream branch, `route.ts`
|
|
21
|
+
* `jsonResponse`) or the SDK's UI message stream (`text/event-stream`,
|
|
22
|
+
* `createUIMessageStreamResponse`). Anything else — an HTML shell, a host's 404
|
|
23
|
+
* page, a proxy's error page — was not produced by the route, so the request did
|
|
24
|
+
* not reach a mounted route at that path. Positive detection (what the route DOES
|
|
25
|
+
* answer) rather than a blocklist of page types, so a new kind of "something else"
|
|
26
|
+
* cannot slip past as success.
|
|
27
|
+
*
|
|
28
|
+
* Bundle discipline: imports nothing from `ai` / `@ai-sdk/*` (pinned in
|
|
29
|
+
* `bundle.test.ts`) — it is a plain `fetch` decorator.
|
|
30
|
+
*/
|
|
31
|
+
/** Media types the mounted route answers with — JSON, or the SDK's UI message stream. */
|
|
32
|
+
const ROUTE_MEDIA_TYPES = new Set(['application/json', 'text/event-stream']);
|
|
33
|
+
/** The bare media type of a `content-type` header (`text/html; charset=utf-8` → `text/html`). */
|
|
34
|
+
export function mediaTypeOf(contentType) {
|
|
35
|
+
if (!contentType)
|
|
36
|
+
return null;
|
|
37
|
+
const bare = contentType.split(';')[0]?.trim().toLowerCase() ?? '';
|
|
38
|
+
return bare.length > 0 ? bare : null;
|
|
39
|
+
}
|
|
40
|
+
/** True when a response's content type is one the AI route itself produces. */
|
|
41
|
+
export function isAiRouteAnswer(contentType) {
|
|
42
|
+
const media = mediaTypeOf(contentType);
|
|
43
|
+
return media !== null && ROUTE_MEDIA_TYPES.has(media);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Thrown by {@link guardAiRouteFetch} when the answer at `endpoint` was not produced
|
|
47
|
+
* by the AI route — no route is mounted there (or something in front of it
|
|
48
|
+
* answered). Carries what a developer needs to act: the path, the status, and the
|
|
49
|
+
* content type that came back. The `message` is self-describing on purpose: the
|
|
50
|
+
* tool executor (`tools.ts`) surfaces `error.message` verbatim as a tool step's
|
|
51
|
+
* error text, and a bare "unavailable" there would hide the cause again.
|
|
52
|
+
*/
|
|
53
|
+
export class AiRouteNotMountedError extends Error {
|
|
54
|
+
endpoint;
|
|
55
|
+
status;
|
|
56
|
+
contentType;
|
|
57
|
+
name = 'AiRouteNotMountedError';
|
|
58
|
+
constructor(endpoint, status, contentType) {
|
|
59
|
+
super(`No AI route is mounted at ${endpoint}: the request was answered with ` +
|
|
60
|
+
`${contentType ?? 'no content type'} (HTTP ${status}) instead of the route's JSON or event stream.`);
|
|
61
|
+
this.endpoint = endpoint;
|
|
62
|
+
this.status = status;
|
|
63
|
+
this.contentType = contentType;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Type guard for {@link AiRouteNotMountedError} (works across bundle copies — checks the name). */
|
|
67
|
+
export function isAiRouteNotMountedError(error) {
|
|
68
|
+
return (error instanceof AiRouteNotMountedError ||
|
|
69
|
+
(typeof error === 'object' &&
|
|
70
|
+
error !== null &&
|
|
71
|
+
error.name === 'AiRouteNotMountedError' &&
|
|
72
|
+
typeof error.endpoint === 'string'));
|
|
73
|
+
}
|
|
74
|
+
/** The path a `fetch` input names (string, `URL`, or `Request`). */
|
|
75
|
+
function endpointOf(input) {
|
|
76
|
+
if (typeof input === 'string')
|
|
77
|
+
return input;
|
|
78
|
+
if (input instanceof URL)
|
|
79
|
+
return input.toString();
|
|
80
|
+
return input.url;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Wrap a `fetch` so a response the AI route could not have produced throws
|
|
84
|
+
* {@link AiRouteNotMountedError} instead of being handed to a JSON/stream parser.
|
|
85
|
+
* Everything else — including the route's own error answers (401/429/502 JSON) —
|
|
86
|
+
* passes through unchanged for the existing handlers.
|
|
87
|
+
*/
|
|
88
|
+
export function guardAiRouteFetch(fetchImpl = fetch) {
|
|
89
|
+
return async (input, init) => {
|
|
90
|
+
const response = await fetchImpl(input, init);
|
|
91
|
+
const contentType = response.headers.get('content-type');
|
|
92
|
+
if (!isAiRouteAnswer(contentType)) {
|
|
93
|
+
throw new AiRouteNotMountedError(endpointOf(input), response.status, contentType);
|
|
94
|
+
}
|
|
95
|
+
return response;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=route-fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-fetch.js","sourceRoot":"","sources":["../../../src/lib/ai/route-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,yFAAyF;AACzF,MAAM,iBAAiB,GAAwB,IAAI,GAAG,CAAC,CAAC,kBAAkB,EAAE,mBAAmB,CAAC,CAAC,CAAA;AAEjG,iGAAiG;AACjG,MAAM,UAAU,WAAW,CAAC,WAAsC;IAChE,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAA;IAC7B,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,IAAI,EAAE,CAAA;IAClE,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA;AACtC,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,eAAe,CAAC,WAAsC;IACpE,MAAM,KAAK,GAAG,WAAW,CAAC,WAAW,CAAC,CAAA;IACtC,OAAO,KAAK,KAAK,IAAI,IAAI,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;AACvD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IAGpC;IACA;IACA;IAJO,IAAI,GAAG,wBAAwB,CAAA;IACjD,YACW,QAAgB,EAChB,MAAc,EACd,WAA0B;QAEnC,KAAK,CACH,6BAA6B,QAAQ,kCAAkC;YACrE,GAAG,WAAW,IAAI,iBAAiB,UAAU,MAAM,gDAAgD,CACtG,CAAA;QAPQ,aAAQ,GAAR,QAAQ,CAAQ;QAChB,WAAM,GAAN,MAAM,CAAQ;QACd,gBAAW,GAAX,WAAW,CAAe;IAMrC,CAAC;CACF;AAED,oGAAoG;AACpG,MAAM,UAAU,wBAAwB,CAAC,KAAc;IACrD,OAAO,CACL,KAAK,YAAY,sBAAsB;QACvC,CAAC,OAAO,KAAK,KAAK,QAAQ;YACxB,KAAK,KAAK,IAAI;YACb,KAA4B,CAAC,IAAI,KAAK,wBAAwB;YAC/D,OAAQ,KAAgC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAClE,CAAA;AACH,CAAC;AAED,oEAAoE;AACpE,SAAS,UAAU,CAAC,KAAwB;IAC1C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC3C,IAAI,KAAK,YAAY,GAAG;QAAE,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAA;IACjD,OAAO,KAAK,CAAC,GAAG,CAAA;AAClB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,YAA0B,KAAK;IAC/D,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAC3B,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;QAC7C,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;QACxD,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,sBAAsB,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;QACnF,CAAC;QACD,OAAO,QAAQ,CAAA;IACjB,CAAC,CAAA;AACH,CAAC","sourcesContent":["/**\n * The AI route's response-shape guard (EC-514 rider): ONE wrapping `fetch` shared by\n * both client call sites of the mounted route — the panel's chat transport\n * (`useChat` + `DefaultChatTransport`, which accepts a `fetch` option) and the\n * structured proxy in `createRouteProvider` — so \"the route is not mounted at this\n * path\" is a typed, nameable failure at both, instead of whatever each transport\n * makes of a page of HTML.\n *\n * Why this exists (measured, EC-514): with the EC-503 dev degrade active (studio\n * `dist` missing → the Vite dev mount is skipped) a `POST /api/ai` falls through to\n * the SPA shell — `200 text/html`. The chat transport treats a 200 as success and\n * parses the HTML as an event stream; the HTML carries no `data:` lines, so the\n * stream ends EMPTY and the panel shows the user's turn followed by nothing — no\n * error note, no assistant turn. A silent failure, the exact state AGENTS.md §4\n * forbids. The structured proxy's path is no better by construction: a 200 is\n * `response.ok`, so it calls `response.json()` on `<!DOCTYPE …` — a parse error\n * that names neither the path nor the cause.\n *\n * The contract this guard enforces is the route's OWN: `createAiRouteHandler` only\n * ever answers `application/json` (every non-stream branch, `route.ts`\n * `jsonResponse`) or the SDK's UI message stream (`text/event-stream`,\n * `createUIMessageStreamResponse`). Anything else — an HTML shell, a host's 404\n * page, a proxy's error page — was not produced by the route, so the request did\n * not reach a mounted route at that path. Positive detection (what the route DOES\n * answer) rather than a blocklist of page types, so a new kind of \"something else\"\n * cannot slip past as success.\n *\n * Bundle discipline: imports nothing from `ai` / `@ai-sdk/*` (pinned in\n * `bundle.test.ts`) — it is a plain `fetch` decorator.\n */\n\n/** Media types the mounted route answers with — JSON, or the SDK's UI message stream. */\nconst ROUTE_MEDIA_TYPES: ReadonlySet<string> = new Set(['application/json', 'text/event-stream'])\n\n/** The bare media type of a `content-type` header (`text/html; charset=utf-8` → `text/html`). */\nexport function mediaTypeOf(contentType: string | null | undefined): string | null {\n if (!contentType) return null\n const bare = contentType.split(';')[0]?.trim().toLowerCase() ?? ''\n return bare.length > 0 ? bare : null\n}\n\n/** True when a response's content type is one the AI route itself produces. */\nexport function isAiRouteAnswer(contentType: string | null | undefined): boolean {\n const media = mediaTypeOf(contentType)\n return media !== null && ROUTE_MEDIA_TYPES.has(media)\n}\n\n/**\n * Thrown by {@link guardAiRouteFetch} when the answer at `endpoint` was not produced\n * by the AI route — no route is mounted there (or something in front of it\n * answered). Carries what a developer needs to act: the path, the status, and the\n * content type that came back. The `message` is self-describing on purpose: the\n * tool executor (`tools.ts`) surfaces `error.message` verbatim as a tool step's\n * error text, and a bare \"unavailable\" there would hide the cause again.\n */\nexport class AiRouteNotMountedError extends Error {\n override readonly name = 'AiRouteNotMountedError'\n constructor(\n readonly endpoint: string,\n readonly status: number,\n readonly contentType: string | null,\n ) {\n super(\n `No AI route is mounted at ${endpoint}: the request was answered with ` +\n `${contentType ?? 'no content type'} (HTTP ${status}) instead of the route's JSON or event stream.`,\n )\n }\n}\n\n/** Type guard for {@link AiRouteNotMountedError} (works across bundle copies — checks the name). */\nexport function isAiRouteNotMountedError(error: unknown): error is AiRouteNotMountedError {\n return (\n error instanceof AiRouteNotMountedError ||\n (typeof error === 'object' &&\n error !== null &&\n (error as { name?: unknown }).name === 'AiRouteNotMountedError' &&\n typeof (error as { endpoint?: unknown }).endpoint === 'string')\n )\n}\n\n/** The path a `fetch` input names (string, `URL`, or `Request`). */\nfunction endpointOf(input: RequestInfo | URL): string {\n if (typeof input === 'string') return input\n if (input instanceof URL) return input.toString()\n return input.url\n}\n\n/**\n * Wrap a `fetch` so a response the AI route could not have produced throws\n * {@link AiRouteNotMountedError} instead of being handed to a JSON/stream parser.\n * Everything else — including the route's own error answers (401/429/502 JSON) —\n * passes through unchanged for the existing handlers.\n */\nexport function guardAiRouteFetch(fetchImpl: typeof fetch = fetch): typeof fetch {\n return async (input, init) => {\n const response = await fetchImpl(input, init)\n const contentType = response.headers.get('content-type')\n if (!isAiRouteAnswer(contentType)) {\n throw new AiRouteNotMountedError(endpointOf(input), response.status, contentType)\n }\n return response\n }\n}\n"]}
|
|
@@ -1,31 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `createRouteProvider` — the CLIENT-side {@link AiProvider} the studio runs in the
|
|
3
|
-
* browser (EC-199). It is a thin proxy that forwards `generateStructured` to the
|
|
4
|
-
* host's mounted {@link createAiRouteHandler} endpoint over `fetch`. The provider
|
|
5
|
-
* KEY never touches the browser: the key-holding {@link createAnthropicProvider}
|
|
6
|
-
* lives only inside the server route; the studio's `defineStudio({ ai: { provider } })`
|
|
7
|
-
* gets this route provider (or the FakeProvider for a keyless playground), so the
|
|
8
|
-
* whole authoring loop — `applyContentWithShape` included — proxies its model calls
|
|
9
|
-
* through the session-gated route while the tools (search / list / apply) execute
|
|
10
|
-
* client-side with the user's own session. It exposes NO `languageModel()` — the
|
|
11
|
-
* conversational loop talks to the route's native chat branch directly (EC-334,
|
|
12
|
-
* `useChat` + `DefaultChatTransport`), not through this provider; the provider
|
|
13
|
-
* only NAMES that target ({@link AiChatTarget}) so endpoint + session headers are
|
|
14
|
-
* configured once.
|
|
15
|
-
*
|
|
16
|
-
* Bundle-safe by construction: this module imports nothing from `ai` / `@ai-sdk/*`
|
|
17
|
-
* (it only uses `fetch`), so it stays off the AI-SDK dependency edge the bundle test
|
|
18
|
-
* pins — the route provider can sit on the studio critical path without pulling the
|
|
19
|
-
* SDK. The capability gate passes: the route only ever fronts a gate-checked provider
|
|
20
|
-
* (validated at the server mount), so the proxy declares both required capabilities.
|
|
21
|
-
*/
|
|
22
1
|
import type { AiProvider } from './types.js';
|
|
23
2
|
/** How extra request headers are supplied — a static set or a (possibly async) thunk. */
|
|
24
3
|
export type RouteHeaders = HeadersInit | (() => HeadersInit | undefined | Promise<HeadersInit | undefined>);
|
|
25
4
|
export interface RouteProviderOptions {
|
|
26
5
|
/** The mounted route path/URL (default `/api/ai`). */
|
|
27
6
|
endpoint?: string;
|
|
28
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* `fetch` implementation (default the global). Injected in tests. Always wrapped
|
|
9
|
+
* by {@link guardAiRouteFetch} (EC-514): an answer the route could not have
|
|
10
|
+
* produced — the SPA shell, a host 404 page — throws `AiRouteNotMountedError`.
|
|
11
|
+
*/
|
|
29
12
|
fetchImpl?: typeof fetch;
|
|
30
13
|
/**
|
|
31
14
|
* Extra headers per request — e.g. the studio session token the route's `getSession`
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createRouteProvider` — the CLIENT-side {@link AiProvider} the studio runs in the
|
|
3
|
+
* browser (EC-199). It is a thin proxy that forwards `generateStructured` to the
|
|
4
|
+
* host's mounted {@link createAiRouteHandler} endpoint over `fetch`. The provider
|
|
5
|
+
* KEY never touches the browser: the key-holding {@link createAnthropicProvider}
|
|
6
|
+
* lives only inside the server route; the studio's `defineStudio({ ai: { provider } })`
|
|
7
|
+
* gets this route provider (or the FakeProvider for a keyless playground), so the
|
|
8
|
+
* whole authoring loop — `applyContentWithShape` included — proxies its model calls
|
|
9
|
+
* through the session-gated route while the tools (search / list / apply) execute
|
|
10
|
+
* client-side with the user's own session. It exposes NO `languageModel()` — the
|
|
11
|
+
* conversational loop talks to the route's native chat branch directly (EC-334,
|
|
12
|
+
* `useChat` + `DefaultChatTransport`), not through this provider; the provider
|
|
13
|
+
* only NAMES that target ({@link AiChatTarget}) so endpoint + session headers are
|
|
14
|
+
* configured once.
|
|
15
|
+
*
|
|
16
|
+
* Bundle-safe by construction: this module imports nothing from `ai` / `@ai-sdk/*`
|
|
17
|
+
* (it only uses `fetch`), so it stays off the AI-SDK dependency edge the bundle test
|
|
18
|
+
* pins — the route provider can sit on the studio critical path without pulling the
|
|
19
|
+
* SDK. The capability gate passes: the route only ever fronts a gate-checked provider
|
|
20
|
+
* (validated at the server mount), so the proxy declares both required capabilities.
|
|
21
|
+
*/
|
|
22
|
+
import { guardAiRouteFetch } from './route-fetch.js';
|
|
1
23
|
/** Default mount path a consuming studio app exposes for {@link createAiRouteHandler}. */
|
|
2
24
|
export const DEFAULT_AI_ENDPOINT = '/api/ai';
|
|
3
25
|
/**
|
|
@@ -44,7 +66,7 @@ async function errorCode(response) {
|
|
|
44
66
|
*/
|
|
45
67
|
export function createRouteProvider(options = {}) {
|
|
46
68
|
const endpoint = options.endpoint ?? DEFAULT_AI_ENDPOINT;
|
|
47
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
69
|
+
const fetchImpl = guardAiRouteFetch(options.fetchImpl ?? fetch);
|
|
48
70
|
async function post(body) {
|
|
49
71
|
const extra = await resolveHeaders(options.headers);
|
|
50
72
|
const headers = new Headers(extra);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-provider.js","sourceRoot":"","sources":["../../../src/lib/ai/route-provider.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"route-provider.js","sourceRoot":"","sources":["../../../src/lib/ai/route-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAiCjD,0FAA0F;AAC1F,MAAM,CAAC,MAAM,mBAAmB,GAAG,SAAS,CAAA;AAmB5C;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAA2B;IAC3D,MAAM,SAAS,GAAI,QAA4C,EAAE,UAAU,CAAA;IAC3E,IAAI,SAAS,IAAI,OAAO,SAAS,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAA;IACzE,OAAO,EAAE,QAAQ,EAAE,mBAAmB,EAAE,CAAA;AAC1C,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,OAAiC;IAC7D,IAAI,OAAO,OAAO,KAAK,UAAU;QAAE,OAAO,OAAO,EAAE,CAAA;IACnD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,kFAAkF;AAClF,MAAM,OAAO,YAAa,SAAQ,KAAK;IAE1B;IACA;IAFX,YACW,MAAc,EACd,IAAY;QAErB,KAAK,CAAC,4BAA4B,MAAM,KAAK,IAAI,GAAG,CAAC,CAAA;QAH5C,WAAM,GAAN,MAAM,CAAQ;QACd,SAAI,GAAJ,IAAI,CAAQ;QAGrB,IAAI,CAAC,IAAI,GAAG,cAAc,CAAA;IAC5B,CAAC;CACF;AAED,KAAK,UAAU,SAAS,CAAC,QAAkB;IACzC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAwB,CAAA;QAC3D,OAAO,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,gBAAgB,CAAA;IACvE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,gBAAgB,CAAA;IACzB,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,UAAgC,EAAE;IACpE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,mBAAmB,CAAA;IACxD,MAAM,SAAS,GAAG,iBAAiB,CAAC,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC,CAAA;IAE/D,KAAK,UAAU,IAAI,CAAC,IAAa;QAC/B,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;QACnD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,KAAK,CAAC,CAAA;QAClC,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAA;QAC/C,OAAO,SAAS,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACrF,CAAC;IAED,OAAO;QACL,EAAE,EAAE,OAAO,CAAC,EAAE,IAAI,OAAO;QACzB,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChE,UAAU,EAAE,EAAE,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE;QAChG,YAAY,EAAE,GAA2B,EAAE,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC;QAE3F,KAAK,CAAC,kBAAkB,CACtB,OAAkC;YAElC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAA;YAC1D,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,MAAM,IAAI,YAAY,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAA;YACpF,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA6B,CAAA;QAC5D,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["/**\n * `createRouteProvider` — the CLIENT-side {@link AiProvider} the studio runs in the\n * browser (EC-199). It is a thin proxy that forwards `generateStructured` to the\n * host's mounted {@link createAiRouteHandler} endpoint over `fetch`. The provider\n * KEY never touches the browser: the key-holding {@link createAnthropicProvider}\n * lives only inside the server route; the studio's `defineStudio({ ai: { provider } })`\n * gets this route provider (or the FakeProvider for a keyless playground), so the\n * whole authoring loop — `applyContentWithShape` included — proxies its model calls\n * through the session-gated route while the tools (search / list / apply) execute\n * client-side with the user's own session. It exposes NO `languageModel()` — the\n * conversational loop talks to the route's native chat branch directly (EC-334,\n * `useChat` + `DefaultChatTransport`), not through this provider; the provider\n * only NAMES that target ({@link AiChatTarget}) so endpoint + session headers are\n * configured once.\n *\n * Bundle-safe by construction: this module imports nothing from `ai` / `@ai-sdk/*`\n * (it only uses `fetch`), so it stays off the AI-SDK dependency edge the bundle test\n * pins — the route provider can sit on the studio critical path without pulling the\n * SDK. The capability gate passes: the route only ever fronts a gate-checked provider\n * (validated at the server mount), so the proxy declares both required capabilities.\n */\nimport { guardAiRouteFetch } from './route-fetch'\nimport type {\n AiProvider,\n AiProviderCapabilities,\n GenerateStructuredRequest,\n GenerateStructuredResult,\n} from './types'\n\n/** How extra request headers are supplied — a static set or a (possibly async) thunk. */\nexport type RouteHeaders =\n | HeadersInit\n | (() => HeadersInit | undefined | Promise<HeadersInit | undefined>)\n\nexport interface RouteProviderOptions {\n /** The mounted route path/URL (default `/api/ai`). */\n endpoint?: string\n /**\n * `fetch` implementation (default the global). Injected in tests. Always wrapped\n * by {@link guardAiRouteFetch} (EC-514): an answer the route could not have\n * produced — the SPA shell, a host 404 page — throws `AiRouteNotMountedError`.\n */\n fetchImpl?: typeof fetch\n /**\n * Extra headers per request — e.g. the studio session token the route's `getSession`\n * checks. A thunk is re-evaluated on every call so a refreshed token is picked up.\n */\n headers?: RouteHeaders\n /** Provider id recorded in provenance (default `route`). */\n id?: string\n /** Model id recorded in provenance (the downstream model, when the host fixes one). */\n model?: string\n}\n\n/** Default mount path a consuming studio app exposes for {@link createAiRouteHandler}. */\nexport const DEFAULT_AI_ENDPOINT = '/api/ai'\n\n/**\n * The chat transport target (EC-334): where the panel's `useChat` posts its\n * native `{ messages, context }` requests, and which extra headers ride along\n * (the session token thunk). `createRouteProvider` exposes its own endpoint +\n * headers here, so the ONE configured provider is the single source of truth\n * for both the structured proxy and the chat transport.\n */\nexport interface AiChatTarget {\n endpoint: string\n headers?: RouteHeaders\n}\n\n/** An {@link AiProvider} that also names the chat transport target it fronts. */\nexport interface RouteAiProvider extends AiProvider {\n readonly chatTarget: AiChatTarget\n}\n\n/**\n * The chat target behind a configured provider: the route provider's own\n * endpoint + headers, or the default mount path for any other provider (the\n * chat loop always talks to the mounted route — a missing mount fails loud as\n * a visible route error in the panel, never silently).\n */\nexport function resolveChatTarget(provider: AiProvider | null): AiChatTarget {\n const candidate = (provider as Partial<RouteAiProvider> | null)?.chatTarget\n if (candidate && typeof candidate.endpoint === 'string') return candidate\n return { endpoint: DEFAULT_AI_ENDPOINT }\n}\n\nasync function resolveHeaders(headers: RouteHeaders | undefined): Promise<HeadersInit | undefined> {\n if (typeof headers === 'function') return headers()\n return headers\n}\n\n/** A structured error thrown when the route rejects (surfaced by the chat UI). */\nexport class AiRouteError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n ) {\n super(`AI route request failed (${status}: ${code})`)\n this.name = 'AiRouteError'\n }\n}\n\nasync function errorCode(response: Response): Promise<string> {\n try {\n const body = (await response.json()) as { error?: unknown }\n return typeof body.error === 'string' ? body.error : 'request-failed'\n } catch {\n return 'request-failed'\n }\n}\n\n/**\n * Build a route-backed {@link AiProvider}. Deterministic given a deterministic route\n * (so the FakeProvider mounted behind the route drives the whole client loop in the\n * playground). Never holds a key; never imports the AI SDK.\n */\nexport function createRouteProvider(options: RouteProviderOptions = {}): RouteAiProvider {\n const endpoint = options.endpoint ?? DEFAULT_AI_ENDPOINT\n const fetchImpl = guardAiRouteFetch(options.fetchImpl ?? fetch)\n\n async function post(body: unknown): Promise<Response> {\n const extra = await resolveHeaders(options.headers)\n const headers = new Headers(extra)\n headers.set('content-type', 'application/json')\n return fetchImpl(endpoint, { method: 'POST', headers, body: JSON.stringify(body) })\n }\n\n return {\n id: options.id ?? 'route',\n ...(options.model !== undefined ? { model: options.model } : {}),\n chatTarget: { endpoint, ...(options.headers !== undefined ? { headers: options.headers } : {}) },\n capabilities: (): AiProviderCapabilities => ({ toolCalling: true, structuredOutput: true }),\n\n async generateStructured(\n request: GenerateStructuredRequest,\n ): Promise<GenerateStructuredResult> {\n const response = await post({ kind: 'generate', request })\n if (!response.ok) throw new AiRouteError(response.status, await errorCode(response))\n return (await response.json()) as GenerateStructuredResult\n },\n }\n}\n"]}
|
|
@@ -6,15 +6,37 @@ import type { ComponentManifest } from '@elytracms/core/component-registry';
|
|
|
6
6
|
* the anchor node's child slot when it has one, else *after* the anchor as a
|
|
7
7
|
* sibling, else appended to the page root. Pure and selection-driven so the
|
|
8
8
|
* canvas, the page tree, and keyboard paste all share one rule.
|
|
9
|
+
*
|
|
10
|
+
* EC-545: the resolved slot's `allow` leash is checked HERE, for the pasted
|
|
11
|
+
* subtree's root, through the same `slotAllows` the drop rule reads — so a
|
|
12
|
+
* component drag-and-drop refuses is refused by clipboard too, and a caller
|
|
13
|
+
* cannot reach an `InsertTarget` without passing the check (the rejection is a
|
|
14
|
+
* separate member of the result union, not a flag beside the target). Nested
|
|
15
|
+
* validity of the pasted subtree stays with the publish gate, exactly like a
|
|
16
|
+
* drop.
|
|
9
17
|
*/
|
|
10
18
|
export interface PasteTarget {
|
|
19
|
+
ok: true;
|
|
11
20
|
target: InsertTarget;
|
|
12
21
|
/** Human label of where the paste lands (notices/debugging). */
|
|
13
22
|
destinationLabel: string;
|
|
14
23
|
}
|
|
24
|
+
export interface PasteRejection {
|
|
25
|
+
ok: false;
|
|
26
|
+
code: 'not-allowed';
|
|
27
|
+
/** The pasted root the slot refuses. */
|
|
28
|
+
componentId: string;
|
|
29
|
+
/** The resolved slot whose `allow` list excludes it. */
|
|
30
|
+
slot: string;
|
|
31
|
+
/** Human label of the refused destination (`Section · body`). */
|
|
32
|
+
destinationLabel: string;
|
|
33
|
+
}
|
|
34
|
+
export type PasteResolution = PasteTarget | PasteRejection;
|
|
15
35
|
export declare function resolvePasteTarget(args: {
|
|
16
36
|
root: ComponentNode;
|
|
17
37
|
/** The anchor node (usually the selection); `null` falls back to the root. */
|
|
18
38
|
anchorId: string | null;
|
|
39
|
+
/** The pasted subtree's root component — the id the slot leash judges. */
|
|
40
|
+
componentId: string;
|
|
19
41
|
getManifest: (componentId: string) => ComponentManifest | undefined;
|
|
20
|
-
}):
|
|
42
|
+
}): PasteResolution;
|
|
@@ -1,29 +1,30 @@
|
|
|
1
1
|
import { getNode, getParentInfo } from '@elytracms/core/project-graph';
|
|
2
|
-
import { manifestRules, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules';
|
|
2
|
+
import { manifestRules, primaryInsertSlot, slotAllows } from '@elytracms/studio/lib/dnd/drop-rules';
|
|
3
3
|
import { shortId } from '@elytracms/studio/lib/persona/labels';
|
|
4
4
|
export function resolvePasteTarget(args) {
|
|
5
|
-
const { root, getManifest } = args;
|
|
5
|
+
const { root, componentId, getManifest } = args;
|
|
6
6
|
const anchor = args.anchorId !== null ? getNode(root, args.anchorId) : undefined;
|
|
7
7
|
const node = anchor ?? root;
|
|
8
|
+
const leashed = (slot, slotRule, target, destinationLabel) => slotAllows(slotRule, componentId)
|
|
9
|
+
? { ok: true, target, destinationLabel }
|
|
10
|
+
: { ok: false, code: 'not-allowed', componentId, slot, destinationLabel };
|
|
8
11
|
// EC-386: the shared slot rule (`primaryInsertSlot` over the merged
|
|
9
12
|
// `manifestRules` vocabulary) — a blocks-prop region (e.g. a Section's `body`)
|
|
10
13
|
// receives the paste exactly like the insert dialog and a DnD `into`.
|
|
11
14
|
const manifest = getManifest(node.componentId);
|
|
12
15
|
const childSlot = primaryInsertSlot(manifestRules(manifest));
|
|
13
16
|
if (childSlot) {
|
|
14
|
-
return {
|
|
15
|
-
target: { parentId: node.id, slot: childSlot.name },
|
|
16
|
-
destinationLabel: `${shortId(node.componentId)} · ${childSlot.name}`,
|
|
17
|
-
};
|
|
17
|
+
return leashed(childSlot.name, childSlot, { parentId: node.id, slot: childSlot.name }, `${shortId(node.componentId)} · ${childSlot.name}`);
|
|
18
18
|
}
|
|
19
19
|
const info = getParentInfo(root, node.id);
|
|
20
20
|
if (info) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
// A sibling paste is governed by the PARENT's slot leash (the drop rule's
|
|
22
|
+
// before/after case). An unknown parent manifest imposes no allow-list;
|
|
23
|
+
// validation owns that issue.
|
|
24
|
+
const parentSlot = manifestRules(getManifest(info.parent.componentId))?.slots.find((s) => s.name === info.slot);
|
|
25
|
+
return leashed(info.slot, parentSlot, { parentId: info.parent.id, slot: info.slot, index: info.index + 1 }, `${shortId(info.parent.componentId)} · ${info.slot}`);
|
|
25
26
|
}
|
|
26
27
|
// Leaf root with no declared slots (degenerate graphs): append to `children`.
|
|
27
|
-
return { target: { parentId: root.id, slot: 'children' }, destinationLabel: 'root' };
|
|
28
|
+
return { ok: true, target: { parentId: root.id, slot: 'children' }, destinationLabel: 'root' };
|
|
28
29
|
}
|
|
29
30
|
//# sourceMappingURL=paste-target.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paste-target.js","sourceRoot":"","sources":["../../../src/lib/clipboard/paste-target.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAGtE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAA;
|
|
1
|
+
{"version":3,"file":"paste-target.js","sourceRoot":"","sources":["../../../src/lib/clipboard/paste-target.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAGtE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,sCAAsC,CAAA;AAEnG,OAAO,EAAE,OAAO,EAAE,MAAM,sCAAsC,CAAA;AAsC9D,MAAM,UAAU,kBAAkB,CAAC,IAOlC;IACC,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,GAAG,IAAI,CAAA;IAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAChF,MAAM,IAAI,GAAG,MAAM,IAAI,IAAI,CAAA;IAE3B,MAAM,OAAO,GAAG,CACd,IAAY,EACZ,QAA8B,EAC9B,MAAoB,EACpB,gBAAwB,EACP,EAAE,CACnB,UAAU,CAAC,QAAQ,EAAE,WAAW,CAAC;QAC/B,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,EAAE;QACxC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,WAAW,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAA;IAE7E,oEAAoE;IACpE,+EAA+E;IAC/E,sEAAsE;IACtE,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IAC9C,MAAM,SAAS,GAAG,iBAAiB,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC5D,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,OAAO,CACZ,SAAS,CAAC,IAAI,EACd,SAAS,EACT,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,EAC3C,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,SAAS,CAAC,IAAI,EAAE,CACnD,CAAA;IACH,CAAC;IAED,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAA;IACzC,IAAI,IAAI,EAAE,CAAC;QACT,0EAA0E;QAC1E,wEAAwE;QACxE,8BAA8B;QAC9B,MAAM,UAAU,GAAG,aAAa,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,CAAA;QAC/G,OAAO,OAAO,CACZ,IAAI,CAAC,IAAI,EACT,UAAU,EACV,EAAE,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,EAAE,EACpE,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CACrD,CAAA;IACH,CAAC;IAED,8EAA8E;IAC9E,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,gBAAgB,EAAE,MAAM,EAAE,CAAA;AAChG,CAAC","sourcesContent":["import { getNode, getParentInfo } from '@elytracms/core/project-graph'\nimport type { ComponentNode, InsertTarget } from '@elytracms/core/project-graph'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { manifestRules, primaryInsertSlot, slotAllows } from '@elytracms/studio/lib/dnd/drop-rules'\nimport type { SlotRule } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { shortId } from '@elytracms/studio/lib/persona/labels'\n\n/**\n * Paste landing-spot resolution (EC-175) — the same placement rule the Insert\n * dialog uses (`insert-context.ts`): a pasted subtree lands *into*\n * the anchor node's child slot when it has one, else *after* the anchor as a\n * sibling, else appended to the page root. Pure and selection-driven so the\n * canvas, the page tree, and keyboard paste all share one rule.\n *\n * EC-545: the resolved slot's `allow` leash is checked HERE, for the pasted\n * subtree's root, through the same `slotAllows` the drop rule reads — so a\n * component drag-and-drop refuses is refused by clipboard too, and a caller\n * cannot reach an `InsertTarget` without passing the check (the rejection is a\n * separate member of the result union, not a flag beside the target). Nested\n * validity of the pasted subtree stays with the publish gate, exactly like a\n * drop.\n */\n\nexport interface PasteTarget {\n ok: true\n target: InsertTarget\n /** Human label of where the paste lands (notices/debugging). */\n destinationLabel: string\n}\n\nexport interface PasteRejection {\n ok: false\n code: 'not-allowed'\n /** The pasted root the slot refuses. */\n componentId: string\n /** The resolved slot whose `allow` list excludes it. */\n slot: string\n /** Human label of the refused destination (`Section · body`). */\n destinationLabel: string\n}\n\nexport type PasteResolution = PasteTarget | PasteRejection\n\nexport function resolvePasteTarget(args: {\n root: ComponentNode\n /** The anchor node (usually the selection); `null` falls back to the root. */\n anchorId: string | null\n /** The pasted subtree's root component — the id the slot leash judges. */\n componentId: string\n getManifest: (componentId: string) => ComponentManifest | undefined\n}): PasteResolution {\n const { root, componentId, getManifest } = args\n const anchor = args.anchorId !== null ? getNode(root, args.anchorId) : undefined\n const node = anchor ?? root\n\n const leashed = (\n slot: string,\n slotRule: SlotRule | undefined,\n target: InsertTarget,\n destinationLabel: string,\n ): PasteResolution =>\n slotAllows(slotRule, componentId)\n ? { ok: true, target, destinationLabel }\n : { ok: false, code: 'not-allowed', componentId, slot, destinationLabel }\n\n // EC-386: the shared slot rule (`primaryInsertSlot` over the merged\n // `manifestRules` vocabulary) — a blocks-prop region (e.g. a Section's `body`)\n // receives the paste exactly like the insert dialog and a DnD `into`.\n const manifest = getManifest(node.componentId)\n const childSlot = primaryInsertSlot(manifestRules(manifest))\n if (childSlot) {\n return leashed(\n childSlot.name,\n childSlot,\n { parentId: node.id, slot: childSlot.name },\n `${shortId(node.componentId)} · ${childSlot.name}`,\n )\n }\n\n const info = getParentInfo(root, node.id)\n if (info) {\n // A sibling paste is governed by the PARENT's slot leash (the drop rule's\n // before/after case). An unknown parent manifest imposes no allow-list;\n // validation owns that issue.\n const parentSlot = manifestRules(getManifest(info.parent.componentId))?.slots.find((s) => s.name === info.slot)\n return leashed(\n info.slot,\n parentSlot,\n { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },\n `${shortId(info.parent.componentId)} · ${info.slot}`,\n )\n }\n\n // Leaf root with no declared slots (degenerate graphs): append to `children`.\n return { ok: true, target: { parentId: root.id, slot: 'children' }, destinationLabel: 'root' }\n}\n"]}
|
|
@@ -10,17 +10,26 @@ import type { CmsDocument, CmsValidationIssue } from '@elytracms/core/cms-core';
|
|
|
10
10
|
* `createStudioOperations` hands this module's reader to the operations layer
|
|
11
11
|
* once, at bootstrap.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* the operations gate
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* **EC-546 — a gate that cannot see says so.** The first cut had the reader return
|
|
14
|
+
* `undefined` with nothing registered, meaning "no session context here", and
|
|
15
|
+
* the operations gate answered that by falling back to an optionless
|
|
16
|
+
* `validateDocument` over the adapter's schema record: scalar rules only, no
|
|
17
|
+
* store, no locale config, no composition or embed leash. Measured: a document
|
|
18
|
+
* carrying a component outside its slot's `allow` published through
|
|
19
|
+
* `createStudioOperations` whenever the stack was empty. That is the exact
|
|
20
|
+
* weaker, mode-dependent gate EC-497 removed, kept alive by a mount-state side
|
|
21
|
+
* channel. So the reader now answers with issues ONLY: with nothing registered it
|
|
22
|
+
* returns one blocking `error` issue — the publish is refused and the refusal is
|
|
23
|
+
* visible — and the operations layer no longer has a fallback to take (its
|
|
24
|
+
* `DocumentValidator` cannot return `undefined`).
|
|
18
25
|
*
|
|
19
26
|
* This module imports nothing but types, so hanging the seam off it never drags
|
|
20
27
|
* the CMS session (or the component registry behind the composition validators)
|
|
21
28
|
* into the operations bootstrap's module graph.
|
|
22
29
|
*/
|
|
23
30
|
export type SessionDocumentValidator = (document: CmsDocument) => readonly CmsValidationIssue[];
|
|
31
|
+
/** The catalog key of the refusal the reader emits with no session mounted (EC-546). */
|
|
32
|
+
export declare const NO_VALIDATION_SESSION_MESSAGE_KEY = "cms.issueMessage.noValidationSession";
|
|
24
33
|
/** Publish the session's canonical document validator (`null` clears the stack). */
|
|
25
34
|
export declare function setSessionDocumentValidator(next: SessionDocumentValidator | null): void;
|
|
26
35
|
/**
|
|
@@ -30,8 +39,9 @@ export declare function setSessionDocumentValidator(next: SessionDocumentValidat
|
|
|
30
39
|
*/
|
|
31
40
|
export declare function clearSessionDocumentValidator(owned: SessionDocumentValidator): void;
|
|
32
41
|
/**
|
|
33
|
-
* The reader handed to `createOperations`.
|
|
34
|
-
*
|
|
35
|
-
* `
|
|
42
|
+
* The reader handed to `createOperations`. The innermost session's verdict, or —
|
|
43
|
+
* with no session mounted — the blocking refusal described on
|
|
44
|
+
* {@link SessionDocumentValidator}. Never `undefined`: the operations layer's
|
|
45
|
+
* `DocumentValidator` has no fallback to offer and no longer accepts one (EC-546).
|
|
36
46
|
*/
|
|
37
|
-
export declare function sessionValidateDocument(document: CmsDocument): readonly CmsValidationIssue[]
|
|
47
|
+
export declare function sessionValidateDocument(document: CmsDocument): readonly CmsValidationIssue[];
|
|
@@ -8,6 +8,25 @@
|
|
|
8
8
|
* published. Popping restores the outer session instead.
|
|
9
9
|
*/
|
|
10
10
|
const registered = [];
|
|
11
|
+
/** The catalog key of the refusal the reader emits with no session mounted (EC-546). */
|
|
12
|
+
export const NO_VALIDATION_SESSION_MESSAGE_KEY = 'cms.issueMessage.noValidationSession';
|
|
13
|
+
/**
|
|
14
|
+
* The verdict with no session mounted: one blocking issue, never a pass and never
|
|
15
|
+
* `undefined`. `custom-validation` is the code for a rule the engine's field
|
|
16
|
+
* vocabulary does not name — this is a host rule ("publish only through a mounted
|
|
17
|
+
* session"), not a finding about the document — and the message carries the
|
|
18
|
+
* meaning. Published via the same `messageRef` the engine's issues use, so the
|
|
19
|
+
* studio's operation-error banner renders it in the active locale (EC-494).
|
|
20
|
+
*/
|
|
21
|
+
const NO_SESSION_REFUSAL = [
|
|
22
|
+
{
|
|
23
|
+
code: 'custom-validation',
|
|
24
|
+
severity: 'error',
|
|
25
|
+
message: 'No validation session is mounted — refusing to publish unvalidated.',
|
|
26
|
+
messageRef: { key: NO_VALIDATION_SESSION_MESSAGE_KEY },
|
|
27
|
+
path: [],
|
|
28
|
+
},
|
|
29
|
+
];
|
|
11
30
|
/** Publish the session's canonical document validator (`null` clears the stack). */
|
|
12
31
|
export function setSessionDocumentValidator(next) {
|
|
13
32
|
if (next === null) {
|
|
@@ -30,11 +49,13 @@ export function clearSessionDocumentValidator(owned) {
|
|
|
30
49
|
registered.splice(at, 1);
|
|
31
50
|
}
|
|
32
51
|
/**
|
|
33
|
-
* The reader handed to `createOperations`.
|
|
34
|
-
*
|
|
35
|
-
* `
|
|
52
|
+
* The reader handed to `createOperations`. The innermost session's verdict, or —
|
|
53
|
+
* with no session mounted — the blocking refusal described on
|
|
54
|
+
* {@link SessionDocumentValidator}. Never `undefined`: the operations layer's
|
|
55
|
+
* `DocumentValidator` has no fallback to offer and no longer accepts one (EC-546).
|
|
36
56
|
*/
|
|
37
57
|
export function sessionValidateDocument(document) {
|
|
38
|
-
|
|
58
|
+
const session = registered.at(-1);
|
|
59
|
+
return session ? session(document) : NO_SESSION_REFUSAL;
|
|
39
60
|
}
|
|
40
61
|
//# sourceMappingURL=document-gate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document-gate.js","sourceRoot":"","sources":["../../../src/lib/cms/document-gate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"document-gate.js","sourceRoot":"","sources":["../../../src/lib/cms/document-gate.ts"],"names":[],"mappings":"AAgCA;;;;;;;;GAQG;AACH,MAAM,UAAU,GAA+B,EAAE,CAAA;AAEjD,wFAAwF;AACxF,MAAM,CAAC,MAAM,iCAAiC,GAAG,sCAAsC,CAAA;AAEvF;;;;;;;GAOG;AACH,MAAM,kBAAkB,GAAkC;IACxD;QACE,IAAI,EAAE,mBAAmB;QACzB,QAAQ,EAAE,OAAO;QACjB,OAAO,EAAE,qEAAqE;QAC9E,UAAU,EAAE,EAAE,GAAG,EAAE,iCAAiC,EAAE;QACtD,IAAI,EAAE,EAAE;KACT;CACF,CAAA;AAED,oFAAoF;AACpF,MAAM,UAAU,2BAA2B,CAAC,IAAqC;IAC/E,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,UAAU,CAAC,MAAM,GAAG,CAAC,CAAA;QACrB,OAAM;IACR,CAAC;IACD,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IACnC,IAAI,EAAE,IAAI,CAAC;QAAE,UAAU,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;IACrC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACvB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAAC,KAA+B;IAC3E,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IACpC,IAAI,EAAE,IAAI,CAAC;QAAE,UAAU,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;AACvC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,QAAqB;IAC3D,MAAM,OAAO,GAAG,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IACjC,OAAO,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAA;AACzD,CAAC","sourcesContent":["import type { CmsDocument, CmsValidationIssue } from '@elytracms/core/cms-core'\n\n/**\n * The publish-gate seam (EC-497) — a framework-free module store, the same shape\n * as the current-document seam (`lib/ai-current-document`).\n *\n * `createOperations` is a module singleton built long before (and independently\n * of) any React tree, while gate-strength validation needs the live CMS session:\n * its document store, its asset oracle, its published-ness pointer. So the CMS\n * session PUBLISHES a validator here while mounted, and\n * `createStudioOperations` hands this module's reader to the operations layer\n * once, at bootstrap.\n *\n * **EC-546 — a gate that cannot see says so.** The first cut had the reader return\n * `undefined` with nothing registered, meaning \"no session context here\", and\n * the operations gate answered that by falling back to an optionless\n * `validateDocument` over the adapter's schema record: scalar rules only, no\n * store, no locale config, no composition or embed leash. Measured: a document\n * carrying a component outside its slot's `allow` published through\n * `createStudioOperations` whenever the stack was empty. That is the exact\n * weaker, mode-dependent gate EC-497 removed, kept alive by a mount-state side\n * channel. So the reader now answers with issues ONLY: with nothing registered it\n * returns one blocking `error` issue — the publish is refused and the refusal is\n * visible — and the operations layer no longer has a fallback to take (its\n * `DocumentValidator` cannot return `undefined`).\n *\n * This module imports nothing but types, so hanging the seam off it never drags\n * the CMS session (or the component registry behind the composition validators)\n * into the operations bootstrap's module graph.\n */\nexport type SessionDocumentValidator = (document: CmsDocument) => readonly CmsValidationIssue[]\n\n/**\n * Registered sessions, innermost LAST — a stack, not a single slot, because more\n * than one `CmsProvider` can be mounted at a time: the \"Use in document…\"\n * transfer dialog mounts its own over the app's\n * (`components/transfer-dialog.tsx:92`). With a single slot, closing that dialog\n * would clear the seam and silently drop the publish gate back to its fallback\n * for the rest of the app's life — a regression invisible until something\n * published. Popping restores the outer session instead.\n */\nconst registered: SessionDocumentValidator[] = []\n\n/** The catalog key of the refusal the reader emits with no session mounted (EC-546). */\nexport const NO_VALIDATION_SESSION_MESSAGE_KEY = 'cms.issueMessage.noValidationSession'\n\n/**\n * The verdict with no session mounted: one blocking issue, never a pass and never\n * `undefined`. `custom-validation` is the code for a rule the engine's field\n * vocabulary does not name — this is a host rule (\"publish only through a mounted\n * session\"), not a finding about the document — and the message carries the\n * meaning. Published via the same `messageRef` the engine's issues use, so the\n * studio's operation-error banner renders it in the active locale (EC-494).\n */\nconst NO_SESSION_REFUSAL: readonly CmsValidationIssue[] = [\n {\n code: 'custom-validation',\n severity: 'error',\n message: 'No validation session is mounted — refusing to publish unvalidated.',\n messageRef: { key: NO_VALIDATION_SESSION_MESSAGE_KEY },\n path: [],\n },\n]\n\n/** Publish the session's canonical document validator (`null` clears the stack). */\nexport function setSessionDocumentValidator(next: SessionDocumentValidator | null): void {\n if (next === null) {\n registered.length = 0\n return\n }\n const at = registered.indexOf(next)\n if (at >= 0) registered.splice(at, 1)\n registered.push(next)\n}\n\n/**\n * Remove this validator — the unmount race guard. Removing an INNER session\n * leaves the outer one in place; removing one that already left is a no-op (a\n * remount registers the new validator before the old effect's cleanup runs).\n */\nexport function clearSessionDocumentValidator(owned: SessionDocumentValidator): void {\n const at = registered.indexOf(owned)\n if (at >= 0) registered.splice(at, 1)\n}\n\n/**\n * The reader handed to `createOperations`. The innermost session's verdict, or —\n * with no session mounted — the blocking refusal described on\n * {@link SessionDocumentValidator}. Never `undefined`: the operations layer's\n * `DocumentValidator` has no fallback to offer and no longer accepts one (EC-546).\n */\nexport function sessionValidateDocument(document: CmsDocument): readonly CmsValidationIssue[] {\n const session = registered.at(-1)\n return session ? session(document) : NO_SESSION_REFUSAL\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine version this studio build was compiled from (EC-552) — the client-side
|
|
3
|
+
* half of the deployed-functions version handshake: the boot gate compares it against
|
|
4
|
+
* what `delivery:engineVersion` answers from the deployment.
|
|
5
|
+
*
|
|
6
|
+
* A MIRROR of `package.json`'s `version`, not the declaration itself: client source
|
|
7
|
+
* cannot import `../../package.json` (outside the tsc build's `rootDir`; moving the
|
|
8
|
+
* root would reshape `dist` and every export path). The parity test beside this file
|
|
9
|
+
* fs-reads the real declaration and fails `pnpm verify` the moment the two diverge,
|
|
10
|
+
* so a release cannot ship with a stale mirror. Release staging bumps this alongside
|
|
11
|
+
* the five `package.json` versions and the template pins.
|
|
12
|
+
*
|
|
13
|
+
* (The convex-side stamp does NOT read this mirror — `convex/delivery.ts` imports the
|
|
14
|
+
* package.json declaration directly; its bundler allows what tsc's rootDir forbids.)
|
|
15
|
+
*/
|
|
16
|
+
export declare const ENGINE_VERSION = "0.0.86";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine version this studio build was compiled from (EC-552) — the client-side
|
|
3
|
+
* half of the deployed-functions version handshake: the boot gate compares it against
|
|
4
|
+
* what `delivery:engineVersion` answers from the deployment.
|
|
5
|
+
*
|
|
6
|
+
* A MIRROR of `package.json`'s `version`, not the declaration itself: client source
|
|
7
|
+
* cannot import `../../package.json` (outside the tsc build's `rootDir`; moving the
|
|
8
|
+
* root would reshape `dist` and every export path). The parity test beside this file
|
|
9
|
+
* fs-reads the real declaration and fails `pnpm verify` the moment the two diverge,
|
|
10
|
+
* so a release cannot ship with a stale mirror. Release staging bumps this alongside
|
|
11
|
+
* the five `package.json` versions and the template pins.
|
|
12
|
+
*
|
|
13
|
+
* (The convex-side stamp does NOT read this mirror — `convex/delivery.ts` imports the
|
|
14
|
+
* package.json declaration directly; its bundler allows what tsc's rootDir forbids.)
|
|
15
|
+
*/
|
|
16
|
+
export const ENGINE_VERSION = '0.0.86';
|
|
17
|
+
//# sourceMappingURL=engine-version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine-version.js","sourceRoot":"","sources":["../../src/lib/engine-version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAA","sourcesContent":["/**\n * The engine version this studio build was compiled from (EC-552) — the client-side\n * half of the deployed-functions version handshake: the boot gate compares it against\n * what `delivery:engineVersion` answers from the deployment.\n *\n * A MIRROR of `package.json`'s `version`, not the declaration itself: client source\n * cannot import `../../package.json` (outside the tsc build's `rootDir`; moving the\n * root would reshape `dist` and every export path). The parity test beside this file\n * fs-reads the real declaration and fails `pnpm verify` the moment the two diverge,\n * so a release cannot ship with a stale mirror. Release staging bumps this alongside\n * the five `package.json` versions and the template pins.\n *\n * (The convex-side stamp does NOT read this mirror — `convex/delivery.ts` imports the\n * package.json declaration directly; its bundler allows what tsc's rootDir forbids.)\n */\nexport const ENGINE_VERSION = '0.0.86'\n"]}
|
|
@@ -535,6 +535,7 @@ export const deMessages = {
|
|
|
535
535
|
'cms.issueMessage.preConvergenceV1': 'Das Feld „{field}“ enthält eine Inhaltsform von vor der Konvergenz (die v1-Hülle „{ version, doc }“) — die Engine liest nur noch die konvergierte Sequenz „{ version: 2, content }“. Dieser Inhalt muss neu eingespielt werden (Re-Seed).',
|
|
536
536
|
'cms.issueMessage.preConvergenceBareBlocks': 'Das Feld „{field}“ enthält eine Inhaltsform von vor der Konvergenz (eine blanke Komponentenliste) — die Engine liest nur noch die konvergierte Sequenz „{ version: 2, content }“. Dieser Inhalt muss neu eingespielt werden (Re-Seed).',
|
|
537
537
|
'cms.issueMessage.preConvergencePlaintext': 'Das Feld „{field}“ enthält eine Inhaltsform von vor der Konvergenz (unstrukturierter Alttext) — die Engine liest nur noch die konvergierte Sequenz „{ version: 2, content }“. Dieser Inhalt muss neu eingespielt werden (Re-Seed).',
|
|
538
|
+
'cms.issueMessage.noValidationSession': 'Es ist keine Validierungssitzung eingebunden — die Veröffentlichung ohne Prüfung wird verweigert.',
|
|
538
539
|
'cms.issueMessage.richTextHoldsArray': 'Das Rich-Text-Feld „{field}“ erwartet einen Rich-Text-Wert („{ version: 2, content }“), enthält aber ein Array.',
|
|
539
540
|
'cms.issueMessage.richTextHoldsBareDoc': 'Das Rich-Text-Feld „{field}“ erwartet einen Rich-Text-Wert („{ version: 2, content }“), enthält aber ein blankes Rich-Text-Dokument ohne die Hülle „{ version: 2, content }“.',
|
|
540
541
|
'cms.issueMessage.richTextHoldsObject': 'Das Rich-Text-Feld „{field}“ erwartet einen Rich-Text-Wert („{ version: 2, content }“), enthält aber ein Objekt mit den Schlüsseln „{keys}“.',
|
|
@@ -1071,6 +1072,7 @@ export const deMessages = {
|
|
|
1071
1072
|
'builder.clipboard.copied': '{label} kopiert',
|
|
1072
1073
|
'builder.clipboard.cutNotice': '{label} ausgeschnitten',
|
|
1073
1074
|
'builder.clipboard.pastedInto': '{summary} · in {destination}',
|
|
1075
|
+
'builder.clipboard.notAllowed': '{component} kann nicht in {destination} eingefügt werden — der Slot lässt es nicht zu.',
|
|
1074
1076
|
'builder.clipboard.issueComponent': 'Komponente',
|
|
1075
1077
|
'builder.clipboard.issueBinding': 'Bindung',
|
|
1076
1078
|
// EC-494 — Zwischenablage-Fehler aus der Engine plus der studio-seitige Einfüge-Diff.
|
|
@@ -1158,6 +1160,7 @@ export const deMessages = {
|
|
|
1158
1160
|
'builder.canvasTheme.stylesheetErrorNotice': 'Das Canvas-Stylesheet konnte nicht geladen werden — Blöcke werden ungestylt dargestellt. Prüfen Sie den canvas.css-Build des Studios.',
|
|
1159
1161
|
'builder.canvasTheme.noThemeTokensNotice': 'Das Canvas-Stylesheet definiert keine Theme-Tokens. canvas.css sollte den Full-Tailwind-Theme-Einstieg der Site importieren (siehe docs/theming.md).',
|
|
1160
1162
|
'builder.canvasTheme.noStylesheetNotice': 'Es ist kein Canvas-Stylesheet konfiguriert — Blöcke werden ungestylt dargestellt. Die App muss eines über setCanvasStylesheetUrl bereitstellen (das create-elytra-Template verdrahtet das).',
|
|
1163
|
+
'builder.composition.preservedNotice': 'Diese Komposition enthält {count} Textelement(e), die der Canvas noch nicht darstellen kann. Sie behalten ihre Position zwischen den Blöcken, bleiben von Canvas-Änderungen unberührt und werden normal veröffentlicht.',
|
|
1161
1164
|
'builder.spacing.gapAfterChild': 'Abstand nach Element {index}',
|
|
1162
1165
|
'builder.richText.toolbarLabel': 'Textformatierung',
|
|
1163
1166
|
'builder.richText.linkPlaceholder': 'https://… (leer entfernt)',
|
|
@@ -1266,6 +1269,7 @@ export const deMessages = {
|
|
|
1266
1269
|
'ai.proposal.expandReview': 'Prüfansicht erweitern',
|
|
1267
1270
|
'ai.proposal.collapseReview': 'Prüfansicht schließen',
|
|
1268
1271
|
'ai.error.route': 'Der Assistent ist derzeit nicht verfügbar. Bitte versuchen Sie es erneut.',
|
|
1272
|
+
'ai.error.routeNotMounted': 'Unter {endpoint} ist keine Assistenten-Route eingebunden – die Anfrage wurde von der Seite statt von der KI-Route beantwortet. Prüfen Sie die Routen-Einbindung des Hosts; auf einem Dev-Server fehlt möglicherweise die Build-Ausgabe des Studios.',
|
|
1269
1273
|
'ai.error.generic': 'Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut.',
|
|
1270
1274
|
'ai.error.applyFailed': 'Der Entwurf konnte nicht übernommen werden.',
|
|
1271
1275
|
'ai.error.applyInvalid': 'Der Vorschlag passt nicht mehr zum aktuellen Dokument: {issues}',
|