@elytracms/studio 0.0.57 → 0.0.59
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/dist/components/ai/ai-chat-panel.js +44 -6
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/chat-ui.d.ts +1 -1
- package/dist/components/ai/chat-ui.js +11 -2
- package/dist/components/ai/chat-ui.js.map +1 -1
- package/dist/components/ai/proposal-card.d.ts +30 -7
- package/dist/components/ai/proposal-card.js +81 -21
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/builder/blocks-model.js +12 -7
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +59 -0
- package/dist/components/builder/canvas-dnd.js +207 -0
- package/dist/components/builder/canvas-dnd.js.map +1 -0
- package/dist/components/builder/canvas-dom.d.ts +11 -0
- package/dist/components/builder/canvas-dom.js +17 -0
- package/dist/components/builder/canvas-dom.js.map +1 -0
- package/dist/components/builder/canvas-inline-editing.d.ts +91 -0
- package/dist/components/builder/canvas-inline-editing.js +242 -0
- package/dist/components/builder/canvas-inline-editing.js.map +1 -0
- package/dist/components/builder/canvas-render-context.d.ts +83 -0
- package/dist/components/builder/canvas-render-context.js +196 -0
- package/dist/components/builder/canvas-render-context.js.map +1 -0
- package/dist/components/builder/canvas.d.ts +4 -1
- package/dist/components/builder/canvas.js +32 -567
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +14 -4
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +5 -2
- package/dist/components/builder/insert-context.js +1 -3
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/node-factory.d.ts +12 -0
- package/dist/components/builder/node-factory.js +18 -1
- package/dist/components/builder/node-factory.js.map +1 -1
- package/dist/components/cms/asset-field.d.ts +22 -0
- package/dist/components/cms/asset-field.js +300 -0
- package/dist/components/cms/asset-field.js.map +1 -0
- package/dist/components/cms/detail-editor.js +12 -0
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-diff-view.d.ts +12 -1
- package/dist/components/cms/document-diff-view.js +119 -42
- package/dist/components/cms/document-diff-view.js.map +1 -1
- package/dist/components/cms/field-control-lib.d.ts +43 -0
- package/dist/components/cms/field-control-lib.js +98 -0
- package/dist/components/cms/field-control-lib.js.map +1 -0
- package/dist/components/cms/field-control.d.ts +7 -35
- package/dist/components/cms/field-control.js +13 -556
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/object-array-field.d.ts +29 -0
- package/dist/components/cms/object-array-field.js +205 -0
- package/dist/components/cms/object-array-field.js.map +1 -0
- package/dist/components/cms/rich-text-preview.d.ts +15 -0
- package/dist/components/cms/rich-text-preview.js +81 -0
- package/dist/components/cms/rich-text-preview.js.map +1 -0
- package/dist/components/cms/version-compare-text.d.ts +26 -0
- package/dist/components/cms/version-compare-text.js +60 -0
- package/dist/components/cms/version-compare-text.js.map +1 -1
- package/dist/components/project-session.d.ts +19 -0
- package/dist/components/project-session.js +29 -2
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/use-hydration-phase.d.ts +6 -4
- package/dist/components/use-hydration-phase.js.map +1 -1
- package/dist/lib/ai/apply-content.js +52 -10
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +19 -3
- package/dist/lib/ai/chat-context.js +33 -4
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +6 -3
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/fake-provider.d.ts +60 -5
- package/dist/lib/ai/fake-provider.js +74 -6
- package/dist/lib/ai/fake-provider.js.map +1 -1
- package/dist/lib/ai/index.d.ts +2 -0
- package/dist/lib/ai/index.js +2 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/rich-text-input.d.ts +27 -0
- package/dist/lib/ai/rich-text-input.js +25 -0
- package/dist/lib/ai/rich-text-input.js.map +1 -0
- package/dist/lib/ai/shape.d.ts +12 -1
- package/dist/lib/ai/shape.js +46 -20
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +88 -13
- package/dist/lib/ai/tools.js +253 -14
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/types.d.ts +20 -0
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.d.ts +68 -0
- package/dist/lib/ai/update-fields.js +296 -0
- package/dist/lib/ai/update-fields.js.map +1 -0
- package/dist/lib/ai-current-document.d.ts +53 -0
- package/dist/lib/ai-current-document.js +54 -0
- package/dist/lib/ai-current-document.js.map +1 -0
- package/dist/lib/cms-views/view-model.d.ts +6 -6
- package/dist/lib/data/collection-content.d.ts +12 -7
- package/dist/lib/data/collection-content.js +6 -4
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/i18n/messages.en.js +10 -1
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/session-hydration.d.ts +4 -1
- package/dist/lib/operations/session-hydration.js +4 -1
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/persistence/index.js +13 -1
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +1 -1
- package/dist/lib/workspace/index.js +1 -1
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/workspace-source.d.ts +17 -22
- package/dist/lib/workspace/workspace-source.js +1 -49
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/package.json +2 -2
package/dist/lib/ai/tools.d.ts
CHANGED
|
@@ -14,13 +14,19 @@
|
|
|
14
14
|
* a write path; apply is a separate, human-gated draft write.
|
|
15
15
|
* 3. {@link createChatToolExecutor} — the client executor map `useChat`'s
|
|
16
16
|
* `onToolCall` dispatches into. Readers pass through; the terminal tools run
|
|
17
|
-
* the deterministic engines (`applyContentWithShape` / `translateDocument`
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* the deterministic engines (`applyContentWithShape` / `translateDocument` /
|
|
18
|
+
* `buildUpdateProposal`) and return a COMPACT proposal summary as the tool
|
|
19
|
+
* output — the full proposal goes to the review UI via
|
|
20
20
|
* `onProposal(toolCallId, …)`, never to the model.
|
|
21
21
|
*
|
|
22
|
-
* EC-378
|
|
23
|
-
*
|
|
22
|
+
* EC-378: `updateFields` is the edit-flow terminal tool — the model authors
|
|
23
|
+
* per-field changes directly (it has `getDocument` output + the conversation as
|
|
24
|
+
* context); the executor reads the CURRENT document, builds a validated update
|
|
25
|
+
* proposal (`buildUpdateProposal`), and hands it to the review sink. Its target
|
|
26
|
+
* (and `getDocument`'s) may be omitted to mean the document currently open in
|
|
27
|
+
* the editor — resolved via the {@link ChatToolExecutorOptions.currentDocument}
|
|
28
|
+
* seam ({@link resolveDocumentTarget}); the context block the panel injects is
|
|
29
|
+
* ADVISORY, the resolution here is what makes "this document" deterministic.
|
|
24
30
|
*
|
|
25
31
|
* The EC-199 planner enum/schema (`plannerSchema` / `parsePlannerAction`) retired
|
|
26
32
|
* with the hand-rolled orchestrator (EC-334 slice 2).
|
|
@@ -29,7 +35,7 @@ import { z } from 'zod';
|
|
|
29
35
|
import type { CmsDocument, CollectionDef, CollectionRegistry, LocaleConfig, ValidateDocumentOptions } from '@elytracms/core/cms-core';
|
|
30
36
|
import type { ComponentManifest } from '@elytracms/core/component-registry';
|
|
31
37
|
import type { ChatProposal } from './chat-state.js';
|
|
32
|
-
import type { AiProvider, RelationCandidates, ResolvedKnowledge } from './types.js';
|
|
38
|
+
import type { AiProvider, AssetCandidates, RelationCandidates, ResolvedKnowledge } from './types.js';
|
|
33
39
|
/** A document the model may reference as a relation target (from a `searchDocuments` read). */
|
|
34
40
|
export interface DocumentSearchResult {
|
|
35
41
|
id: string;
|
|
@@ -108,8 +114,8 @@ export declare const chatToolDefinitions: {
|
|
|
108
114
|
getDocument: {
|
|
109
115
|
description: string;
|
|
110
116
|
inputSchema: z.ZodObject<{
|
|
111
|
-
collection: z.ZodString
|
|
112
|
-
id: z.ZodString
|
|
117
|
+
collection: z.ZodOptional<z.ZodString>;
|
|
118
|
+
id: z.ZodOptional<z.ZodString>;
|
|
113
119
|
}, z.core.$strip>;
|
|
114
120
|
};
|
|
115
121
|
createDocument: {
|
|
@@ -119,6 +125,20 @@ export declare const chatToolDefinitions: {
|
|
|
119
125
|
instruction: z.ZodString;
|
|
120
126
|
}, z.core.$strip>;
|
|
121
127
|
};
|
|
128
|
+
updateFields: {
|
|
129
|
+
description: string;
|
|
130
|
+
inputSchema: z.ZodObject<{
|
|
131
|
+
collection: z.ZodOptional<z.ZodString>;
|
|
132
|
+
id: z.ZodOptional<z.ZodString>;
|
|
133
|
+
changes: z.ZodArray<z.ZodObject<{
|
|
134
|
+
field: z.ZodString;
|
|
135
|
+
locale: z.ZodOptional<z.ZodString>;
|
|
136
|
+
blockId: z.ZodOptional<z.ZodString>;
|
|
137
|
+
prop: z.ZodOptional<z.ZodString>;
|
|
138
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
139
|
+
}, z.core.$strip>>;
|
|
140
|
+
}, z.core.$strip>;
|
|
141
|
+
};
|
|
122
142
|
translateDocument: {
|
|
123
143
|
description: string;
|
|
124
144
|
inputSchema: z.ZodObject<{
|
|
@@ -134,6 +154,33 @@ export interface ChatToolCall {
|
|
|
134
154
|
toolName: string;
|
|
135
155
|
input: unknown;
|
|
136
156
|
}
|
|
157
|
+
/** The coordinates of the document currently open in the editor (EC-378 seam). */
|
|
158
|
+
export interface CurrentDocumentRef {
|
|
159
|
+
collection: string;
|
|
160
|
+
id: string;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Resolve a tool call's target document from its (optional) input coordinates
|
|
164
|
+
* and the current-document seam (EC-378). Deterministic:
|
|
165
|
+
*
|
|
166
|
+
* - both given → use them verbatim;
|
|
167
|
+
* - both omitted → the currently open document (error when none is open);
|
|
168
|
+
* - `collection` given with `id` omitted → the open document ONLY when the
|
|
169
|
+
* collections match — a named collection must never silently cross onto a
|
|
170
|
+
* different open document.
|
|
171
|
+
*
|
|
172
|
+
* Pure — tested directly; `getDocument` and `updateFields` share it.
|
|
173
|
+
*/
|
|
174
|
+
export declare function resolveDocumentTarget(input: {
|
|
175
|
+
collection?: string;
|
|
176
|
+
id?: string;
|
|
177
|
+
}, current: CurrentDocumentRef | null | undefined): {
|
|
178
|
+
ok: true;
|
|
179
|
+
ref: CurrentDocumentRef;
|
|
180
|
+
} | {
|
|
181
|
+
ok: false;
|
|
182
|
+
errorText: string;
|
|
183
|
+
};
|
|
137
184
|
/**
|
|
138
185
|
* A tool execution outcome: an `output` posted back as the tool result, or an
|
|
139
186
|
* `errorText` posted as an `output-error` part (the model sees the failure and
|
|
@@ -147,16 +194,27 @@ export type ChatToolOutcome = {
|
|
|
147
194
|
errorText: string;
|
|
148
195
|
};
|
|
149
196
|
/**
|
|
150
|
-
* The per-conversation
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
197
|
+
* The per-conversation candidate store: every `searchDocuments` result is recorded
|
|
198
|
+
* per relation target collection, every `listAssets` result into one flat library
|
|
199
|
+
* pool (EC-381), and the terminal tools may only reference the ids gathered here
|
|
200
|
+
* (the `applyContentWithShape` / `buildUpdateProposal` enum + drop/loud leash makes
|
|
201
|
+
* a hallucinated relation OR asset id structurally unrepresentable / fail loud).
|
|
202
|
+
* Held by the panel per conversation — a new conversation (EC-380 clear) starts an
|
|
203
|
+
* empty store.
|
|
155
204
|
*/
|
|
156
205
|
export interface ChatCandidateStore {
|
|
157
206
|
record(collection: string, results: readonly DocumentSearchResult[]): void;
|
|
207
|
+
/** Record every `listAssets` result into the flat library pool (EC-381). */
|
|
208
|
+
recordAssets(results: readonly AssetSearchResult[]): void;
|
|
158
209
|
/** Candidates for a target collection's relation fields, keyed by field name. */
|
|
159
210
|
candidatesFor(collection: CollectionDef): RelationCandidates;
|
|
211
|
+
/**
|
|
212
|
+
* Candidates for a target collection's asset/image fields (EC-381), keyed by
|
|
213
|
+
* field name — every asset field shares the one flat searched-asset pool
|
|
214
|
+
* (`listAssets` has no field dimension). Empty when nothing was searched, so the
|
|
215
|
+
* asset field is then omitted from the shape (empty beats wrong).
|
|
216
|
+
*/
|
|
217
|
+
assetCandidatesFor(collection: CollectionDef): AssetCandidates;
|
|
160
218
|
}
|
|
161
219
|
export declare function createChatCandidateStore(): ChatCandidateStore;
|
|
162
220
|
export interface ChatToolExecutorOptions {
|
|
@@ -176,6 +234,13 @@ export interface ChatToolExecutorOptions {
|
|
|
176
234
|
* The tool OUTPUT stays a compact summary; the patch never reaches the model.
|
|
177
235
|
*/
|
|
178
236
|
onProposal: (toolCallId: string, proposal: ChatProposal) => void;
|
|
237
|
+
/**
|
|
238
|
+
* The document currently open in the editor (EC-378 seam) — a thunk read at
|
|
239
|
+
* EXECUTION time, so "this document" resolves against what is open NOW.
|
|
240
|
+
* `getDocument` / `updateFields` fall back to it when the model omits the
|
|
241
|
+
* target (see {@link resolveDocumentTarget}).
|
|
242
|
+
*/
|
|
243
|
+
currentDocument?: () => CurrentDocumentRef | null | undefined;
|
|
179
244
|
/** Current knowledge snapshot (thunk — the body thunk refreshes it per request). */
|
|
180
245
|
knowledge?: () => ResolvedKnowledge | undefined;
|
|
181
246
|
/** Component manifests — context for `allow`-leashed richText/blocks fields. */
|
|
@@ -197,6 +262,16 @@ export interface ChatToolExecutorOptions {
|
|
|
197
262
|
export interface ChatToolExecutor {
|
|
198
263
|
execute(call: ChatToolCall): Promise<ChatToolOutcome>;
|
|
199
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* The MODEL-facing view of a document (EC-379): rich-text field values are
|
|
267
|
+
* projected to markdown strings — but ONLY when the engine's loss-guarded
|
|
268
|
+
* inverse can express them faithfully (subset-only content, round-trip proven).
|
|
269
|
+
* A document carrying embeds, internal `ref` links, or out-of-subset marks keeps
|
|
270
|
+
* its `{ version, doc }` envelope, so nothing the model later echoes back can
|
|
271
|
+
* silently lose structure. Applies to the `getDocument` TOOL OUTPUT only; the
|
|
272
|
+
* proposal/apply paths always read the raw stored document.
|
|
273
|
+
*/
|
|
274
|
+
export declare function projectRichTextAsMarkdown(document: CmsDocument, collection: CollectionDef): CmsDocument;
|
|
200
275
|
/**
|
|
201
276
|
* Build the client executor map. Every entry validates its input against
|
|
202
277
|
* {@link chatToolDefinitions}' zod schema (the route validates UIMessages only
|
package/dist/lib/ai/tools.js
CHANGED
|
@@ -14,21 +14,30 @@
|
|
|
14
14
|
* a write path; apply is a separate, human-gated draft write.
|
|
15
15
|
* 3. {@link createChatToolExecutor} — the client executor map `useChat`'s
|
|
16
16
|
* `onToolCall` dispatches into. Readers pass through; the terminal tools run
|
|
17
|
-
* the deterministic engines (`applyContentWithShape` / `translateDocument`
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* the deterministic engines (`applyContentWithShape` / `translateDocument` /
|
|
18
|
+
* `buildUpdateProposal`) and return a COMPACT proposal summary as the tool
|
|
19
|
+
* output — the full proposal goes to the review UI via
|
|
20
20
|
* `onProposal(toolCallId, …)`, never to the model.
|
|
21
21
|
*
|
|
22
|
-
* EC-378
|
|
23
|
-
*
|
|
22
|
+
* EC-378: `updateFields` is the edit-flow terminal tool — the model authors
|
|
23
|
+
* per-field changes directly (it has `getDocument` output + the conversation as
|
|
24
|
+
* context); the executor reads the CURRENT document, builds a validated update
|
|
25
|
+
* proposal (`buildUpdateProposal`), and hands it to the review sink. Its target
|
|
26
|
+
* (and `getDocument`'s) may be omitted to mean the document currently open in
|
|
27
|
+
* the editor — resolved via the {@link ChatToolExecutorOptions.currentDocument}
|
|
28
|
+
* seam ({@link resolveDocumentTarget}); the context block the panel injects is
|
|
29
|
+
* ADVISORY, the resolution here is what makes "this document" deterministic.
|
|
24
30
|
*
|
|
25
31
|
* The EC-199 planner enum/schema (`plannerSchema` / `parsePlannerAction`) retired
|
|
26
32
|
* with the hand-rolled orchestrator (EC-334 slice 2).
|
|
27
33
|
*/
|
|
28
34
|
import { z } from 'zod';
|
|
29
35
|
import { resolveDocumentTitle } from '@elytracms/core/cms-core';
|
|
36
|
+
import { parseRichTextValue, richTextValueToMarkdownProse } from '@elytracms/core/rich-text';
|
|
30
37
|
import { applyContentWithShape } from './apply-content.js';
|
|
31
38
|
import { translateDocument } from './translate-document.js';
|
|
39
|
+
import { buildUpdateProposal } from './update-fields.js';
|
|
40
|
+
import { aiProvenance } from './provenance.js';
|
|
32
41
|
/** Per-search result cap (protects the wire — results become model context). */
|
|
33
42
|
export const DEFAULT_SEARCH_LIMIT = 20;
|
|
34
43
|
/**
|
|
@@ -66,10 +75,18 @@ export const chatToolDefinitions = {
|
|
|
66
75
|
}),
|
|
67
76
|
},
|
|
68
77
|
getDocument: {
|
|
69
|
-
description: 'Fetch one full document (values + localized values) by collection and
|
|
78
|
+
description: 'Fetch one full document (values + localized values + block ids) by collection and ' +
|
|
79
|
+
'id. Omit both to read the document currently open in the editor. Rich-text field ' +
|
|
80
|
+
'values are returned as markdown strings where faithfully expressible.',
|
|
70
81
|
inputSchema: z.object({
|
|
71
|
-
collection: z
|
|
72
|
-
|
|
82
|
+
collection: z
|
|
83
|
+
.string()
|
|
84
|
+
.optional()
|
|
85
|
+
.describe('Collection id. Omit to target the currently open document.'),
|
|
86
|
+
id: z
|
|
87
|
+
.string()
|
|
88
|
+
.optional()
|
|
89
|
+
.describe('Document id (from a searchDocuments result). Omit to target the currently open document.'),
|
|
73
90
|
}),
|
|
74
91
|
},
|
|
75
92
|
createDocument: {
|
|
@@ -81,9 +98,52 @@ export const chatToolDefinitions = {
|
|
|
81
98
|
instruction: z.string().describe('What to write, refining the user request.'),
|
|
82
99
|
}),
|
|
83
100
|
},
|
|
101
|
+
updateFields: {
|
|
102
|
+
description: 'Change specific fields of an EXISTING document as a reviewable proposal — THE ' +
|
|
103
|
+
'tool for every edit request (change / fix / remove / replace / rewrite wording, ' +
|
|
104
|
+
'values, or a page-builder element). Call getDocument first to see the current ' +
|
|
105
|
+
'values (and block ids), then pass ONLY the fields that change. A block prop ' +
|
|
106
|
+
'(e.g. a Hero headline) is addressed as field + blockId + prop. Omit collection ' +
|
|
107
|
+
'and id to target the document currently open in the editor. A rich-text field ' +
|
|
108
|
+
'takes the FULL new value as a MARKDOWN string (headings, lists, links, bold/italic, ' +
|
|
109
|
+
'blockquotes — no images, code, tables, or HTML).',
|
|
110
|
+
inputSchema: z.object({
|
|
111
|
+
collection: z
|
|
112
|
+
.string()
|
|
113
|
+
.optional()
|
|
114
|
+
.describe('Collection id. Omit to target the currently open document.'),
|
|
115
|
+
id: z
|
|
116
|
+
.string()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe('Document id. Omit to target the currently open document.'),
|
|
119
|
+
changes: z
|
|
120
|
+
.array(z.object({
|
|
121
|
+
field: z.string().describe('Field name from the collection schema.'),
|
|
122
|
+
locale: z
|
|
123
|
+
.string()
|
|
124
|
+
.optional()
|
|
125
|
+
.describe("Locale of a LOCALIZED field (defaults to the editor's working locale)."),
|
|
126
|
+
blockId: z
|
|
127
|
+
.string()
|
|
128
|
+
.optional()
|
|
129
|
+
.describe('For a blocks field: the id of the block to change (from getDocument).'),
|
|
130
|
+
prop: z
|
|
131
|
+
.string()
|
|
132
|
+
.optional()
|
|
133
|
+
.describe('With blockId: the component prop to change.'),
|
|
134
|
+
value: z
|
|
135
|
+
.unknown()
|
|
136
|
+
.optional()
|
|
137
|
+
.describe('The new value (a rich-text field takes a markdown string). null clears the field/prop.'),
|
|
138
|
+
}))
|
|
139
|
+
.min(1)
|
|
140
|
+
.describe('The field changes — only what changes, never the whole document.'),
|
|
141
|
+
}),
|
|
142
|
+
},
|
|
84
143
|
translateDocument: {
|
|
85
|
-
description: 'Fill the
|
|
86
|
-
'get its id).
|
|
144
|
+
description: 'Fill the EMPTY locale values of an existing document (search for it first to ' +
|
|
145
|
+
'get its id). Only for translation requests — never for edits (use updateFields ' +
|
|
146
|
+
'to change existing content). Returns a compact proposal summary.',
|
|
87
147
|
inputSchema: z.object({
|
|
88
148
|
collection: z.string().describe('Collection id.'),
|
|
89
149
|
id: z.string().describe('Document id (from a searchDocuments result).'),
|
|
@@ -91,8 +151,39 @@ export const chatToolDefinitions = {
|
|
|
91
151
|
}),
|
|
92
152
|
},
|
|
93
153
|
};
|
|
154
|
+
/**
|
|
155
|
+
* Resolve a tool call's target document from its (optional) input coordinates
|
|
156
|
+
* and the current-document seam (EC-378). Deterministic:
|
|
157
|
+
*
|
|
158
|
+
* - both given → use them verbatim;
|
|
159
|
+
* - both omitted → the currently open document (error when none is open);
|
|
160
|
+
* - `collection` given with `id` omitted → the open document ONLY when the
|
|
161
|
+
* collections match — a named collection must never silently cross onto a
|
|
162
|
+
* different open document.
|
|
163
|
+
*
|
|
164
|
+
* Pure — tested directly; `getDocument` and `updateFields` share it.
|
|
165
|
+
*/
|
|
166
|
+
export function resolveDocumentTarget(input, current) {
|
|
167
|
+
if (input.collection !== undefined && input.id !== undefined) {
|
|
168
|
+
return { ok: true, ref: { collection: input.collection, id: input.id } };
|
|
169
|
+
}
|
|
170
|
+
if (!current) {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
errorText: 'No target document — pass collection and id, or ask the user to open the document in the editor.',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (input.collection !== undefined && input.collection !== current.collection) {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
errorText: `The open document is in collection "${current.collection}", not "${input.collection}" — pass the document id explicitly.`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return { ok: true, ref: { collection: current.collection, id: input.id ?? current.id } };
|
|
183
|
+
}
|
|
94
184
|
export function createChatCandidateStore() {
|
|
95
185
|
const searched = new Map();
|
|
186
|
+
const searchedAssets = new Map();
|
|
96
187
|
return {
|
|
97
188
|
record(collection, results) {
|
|
98
189
|
const byId = searched.get(collection) ?? new Map();
|
|
@@ -100,6 +191,10 @@ export function createChatCandidateStore() {
|
|
|
100
191
|
byId.set(result.id, result);
|
|
101
192
|
searched.set(collection, byId);
|
|
102
193
|
},
|
|
194
|
+
recordAssets(results) {
|
|
195
|
+
for (const result of results)
|
|
196
|
+
searchedAssets.set(result.id, result);
|
|
197
|
+
},
|
|
103
198
|
candidatesFor(collection) {
|
|
104
199
|
const candidates = {};
|
|
105
200
|
for (const field of collection.fields) {
|
|
@@ -112,6 +207,18 @@ export function createChatCandidateStore() {
|
|
|
112
207
|
}
|
|
113
208
|
return candidates;
|
|
114
209
|
},
|
|
210
|
+
assetCandidatesFor(collection) {
|
|
211
|
+
const candidates = {};
|
|
212
|
+
if (searchedAssets.size === 0)
|
|
213
|
+
return candidates;
|
|
214
|
+
const pool = [...searchedAssets.values()].map((a) => ({ id: a.id, ...(a.title ? { label: a.title } : {}) }));
|
|
215
|
+
for (const field of collection.fields) {
|
|
216
|
+
if (field.type !== 'asset')
|
|
217
|
+
continue;
|
|
218
|
+
candidates[field.name] = pool;
|
|
219
|
+
}
|
|
220
|
+
return candidates;
|
|
221
|
+
},
|
|
115
222
|
};
|
|
116
223
|
}
|
|
117
224
|
function issueSummary(issues) {
|
|
@@ -120,6 +227,45 @@ function issueSummary(issues) {
|
|
|
120
227
|
.map((issue) => issue.message)
|
|
121
228
|
.join('; ');
|
|
122
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* The MODEL-facing view of a document (EC-379): rich-text field values are
|
|
232
|
+
* projected to markdown strings — but ONLY when the engine's loss-guarded
|
|
233
|
+
* inverse can express them faithfully (subset-only content, round-trip proven).
|
|
234
|
+
* A document carrying embeds, internal `ref` links, or out-of-subset marks keeps
|
|
235
|
+
* its `{ version, doc }` envelope, so nothing the model later echoes back can
|
|
236
|
+
* silently lose structure. Applies to the `getDocument` TOOL OUTPUT only; the
|
|
237
|
+
* proposal/apply paths always read the raw stored document.
|
|
238
|
+
*/
|
|
239
|
+
export function projectRichTextAsMarkdown(document, collection) {
|
|
240
|
+
const richTextFields = collection.fields
|
|
241
|
+
.filter((field) => field.type === 'richText')
|
|
242
|
+
.map((field) => field.name);
|
|
243
|
+
if (richTextFields.length === 0)
|
|
244
|
+
return document;
|
|
245
|
+
const mapBucket = (bucket) => {
|
|
246
|
+
let next = bucket;
|
|
247
|
+
for (const name of richTextFields) {
|
|
248
|
+
const value = next[name];
|
|
249
|
+
if (value === undefined || value === null)
|
|
250
|
+
continue;
|
|
251
|
+
const parsed = parseRichTextValue(value);
|
|
252
|
+
if (!parsed.ok)
|
|
253
|
+
continue;
|
|
254
|
+
const markdown = richTextValueToMarkdownProse(parsed.value);
|
|
255
|
+
if (markdown === undefined)
|
|
256
|
+
continue; // not faithfully expressible → keep the envelope
|
|
257
|
+
if (next === bucket)
|
|
258
|
+
next = { ...bucket };
|
|
259
|
+
next[name] = markdown;
|
|
260
|
+
}
|
|
261
|
+
return next;
|
|
262
|
+
};
|
|
263
|
+
const values = mapBucket(document.values);
|
|
264
|
+
const localized = document.localized
|
|
265
|
+
? Object.fromEntries(Object.entries(document.localized).map(([locale, bucket]) => [locale, mapBucket(bucket)]))
|
|
266
|
+
: undefined;
|
|
267
|
+
return { ...document, values, ...(localized ? { localized } : {}) };
|
|
268
|
+
}
|
|
123
269
|
/**
|
|
124
270
|
* Build the client executor map. Every entry validates its input against
|
|
125
271
|
* {@link chatToolDefinitions}' zod schema (the route validates UIMessages only
|
|
@@ -151,17 +297,107 @@ export function createChatToolExecutor(options) {
|
|
|
151
297
|
...(input.query ? { query: input.query } : {}),
|
|
152
298
|
limit,
|
|
153
299
|
})).slice(0, limit);
|
|
300
|
+
// EC-381 — leash asset fields to what the model actually searched, exactly as
|
|
301
|
+
// searchDocuments feeds the relation store.
|
|
302
|
+
options.candidates.recordAssets(results);
|
|
154
303
|
return { ok: true, output: results };
|
|
155
304
|
}
|
|
156
305
|
async function getDocument(input) {
|
|
157
306
|
if (!options.readers.getDocument) {
|
|
158
307
|
return { ok: false, errorText: 'Reading documents is not available here.' };
|
|
159
308
|
}
|
|
160
|
-
const
|
|
309
|
+
const target = resolveDocumentTarget(input, options.currentDocument?.());
|
|
310
|
+
if (!target.ok)
|
|
311
|
+
return { ok: false, errorText: target.errorText };
|
|
312
|
+
const document = await options.readers.getDocument(target.ref);
|
|
161
313
|
if (!document) {
|
|
162
|
-
return {
|
|
314
|
+
return {
|
|
315
|
+
ok: false,
|
|
316
|
+
errorText: `Document "${target.ref.collection}/${target.ref.id}" was not found.`,
|
|
317
|
+
};
|
|
163
318
|
}
|
|
164
|
-
|
|
319
|
+
// EC-379: the model reads (and re-authors) rich text as markdown.
|
|
320
|
+
const collection = collectionById(target.ref.collection);
|
|
321
|
+
return { ok: true, output: collection ? projectRichTextAsMarkdown(document, collection) : document };
|
|
322
|
+
}
|
|
323
|
+
/** EC-378 — the edit-flow terminal tool: current doc + model-authored changes → validated proposal. */
|
|
324
|
+
async function updateFields(toolCallId, input) {
|
|
325
|
+
if (!options.readers.getDocument) {
|
|
326
|
+
return { ok: false, errorText: 'Reading documents is not available here.' };
|
|
327
|
+
}
|
|
328
|
+
const target = resolveDocumentTarget(input, options.currentDocument?.());
|
|
329
|
+
if (!target.ok)
|
|
330
|
+
return { ok: false, errorText: target.errorText };
|
|
331
|
+
const collection = collectionById(target.ref.collection);
|
|
332
|
+
if (!collection) {
|
|
333
|
+
return {
|
|
334
|
+
ok: false,
|
|
335
|
+
errorText: `Unknown collection "${target.ref.collection}" — use a collection id from the catalog.`,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const document = await options.readers.getDocument(target.ref);
|
|
339
|
+
if (!document) {
|
|
340
|
+
return {
|
|
341
|
+
ok: false,
|
|
342
|
+
errorText: `Document "${target.ref.collection}/${target.ref.id}" was not found.`,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const result = buildUpdateProposal({
|
|
346
|
+
registry: options.registry,
|
|
347
|
+
collection,
|
|
348
|
+
document,
|
|
349
|
+
changes: input.changes,
|
|
350
|
+
candidates: options.candidates.candidatesFor(collection),
|
|
351
|
+
assetCandidates: options.candidates.assetCandidatesFor(collection),
|
|
352
|
+
...(options.locale ? { locale: options.locale } : {}),
|
|
353
|
+
...(options.localeConfig ? { localeConfig: options.localeConfig } : {}),
|
|
354
|
+
...(options.validateRichTextEmbeds
|
|
355
|
+
? { validateRichTextEmbeds: options.validateRichTextEmbeds }
|
|
356
|
+
: {}),
|
|
357
|
+
...(options.validateComposition ? { validateComposition: options.validateComposition } : {}),
|
|
358
|
+
...(options.store ? { store: options.store } : {}),
|
|
359
|
+
});
|
|
360
|
+
if (!result.ok) {
|
|
361
|
+
return {
|
|
362
|
+
ok: false,
|
|
363
|
+
errorText: `The proposed changes did not pass validation: ${issueSummary(result.issues)}`,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
if (result.kind === 'no-change') {
|
|
367
|
+
return {
|
|
368
|
+
ok: true,
|
|
369
|
+
output: {
|
|
370
|
+
status: 'no-change',
|
|
371
|
+
note: 'The requested change matches the current values — there is nothing to update.',
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
options.onProposal(toolCallId, {
|
|
376
|
+
origin: 'update',
|
|
377
|
+
collection: collection.id,
|
|
378
|
+
documentId: document.id,
|
|
379
|
+
patch: result.patch,
|
|
380
|
+
provenance: aiProvenance(options.provider),
|
|
381
|
+
attempts: 1,
|
|
382
|
+
issues: result.issues,
|
|
383
|
+
before: result.before,
|
|
384
|
+
});
|
|
385
|
+
// Compact summary only — the full patch goes to the review UI, not the model.
|
|
386
|
+
const titleValues = {
|
|
387
|
+
...result.patch.values,
|
|
388
|
+
...(options.locale ? result.patch.localized?.[options.locale] : {}),
|
|
389
|
+
};
|
|
390
|
+
return {
|
|
391
|
+
ok: true,
|
|
392
|
+
output: {
|
|
393
|
+
status: 'proposed',
|
|
394
|
+
collection: collection.id,
|
|
395
|
+
documentId: document.id,
|
|
396
|
+
title: resolveDocumentTitle(collection, titleValues, document.id),
|
|
397
|
+
changedFields: result.changedFields,
|
|
398
|
+
note: 'Update proposal created; the editor reviews and applies it in the studio. Summarize the change briefly — do not repeat the content.',
|
|
399
|
+
},
|
|
400
|
+
};
|
|
165
401
|
}
|
|
166
402
|
async function createDocument(toolCallId, input) {
|
|
167
403
|
const collection = collectionById(input.collection);
|
|
@@ -179,6 +415,8 @@ export function createChatToolExecutor(options) {
|
|
|
179
415
|
...(knowledge ? { knowledge } : {}),
|
|
180
416
|
...(options.components ? { components: options.components } : {}),
|
|
181
417
|
candidates: options.candidates.candidatesFor(collection),
|
|
418
|
+
// EC-381 — asset/image fields are leashed to searched library assets.
|
|
419
|
+
assetCandidates: options.candidates.assetCandidatesFor(collection),
|
|
182
420
|
...(options.locale ? { locale: options.locale } : {}),
|
|
183
421
|
...(options.validateRichTextEmbeds
|
|
184
422
|
? { validateRichTextEmbeds: options.validateRichTextEmbeds }
|
|
@@ -301,9 +539,10 @@ export function createChatToolExecutor(options) {
|
|
|
301
539
|
return await getDocument(parsed.data);
|
|
302
540
|
case 'createDocument':
|
|
303
541
|
return await createDocument(call.toolCallId, parsed.data);
|
|
542
|
+
case 'updateFields':
|
|
543
|
+
return await updateFields(call.toolCallId, parsed.data);
|
|
304
544
|
case 'translateDocument':
|
|
305
545
|
return await translate(call.toolCallId, parsed.data);
|
|
306
|
-
// EC-378: case 'updateFields' lands here (leashed field-update proposals).
|
|
307
546
|
default:
|
|
308
547
|
return { ok: false, errorText: `Unknown tool "${call.toolName}".` };
|
|
309
548
|
}
|