@elytracms/studio 0.0.95 → 0.0.96
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/cms.ts +124 -8
- package/convex/strict-reference-race.test.ts +139 -0
- package/dist/components/ai/ai-chat-panel.js +15 -2
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/translate-document-action.d.ts +6 -0
- package/dist/components/ai/translate-document-action.js.map +1 -1
- package/dist/components/ai/translate-document-dialog.d.ts +1 -1
- package/dist/components/ai/translate-document-dialog.js +5 -5
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/assets/asset-detail.js +5 -2
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/asset-locale.d.ts +7 -0
- package/dist/components/assets/asset-locale.js +16 -2
- package/dist/components/assets/asset-locale.js.map +1 -1
- package/dist/components/assets/asset-meta-drift-notice.d.ts +13 -0
- package/dist/components/assets/asset-meta-drift-notice.js +22 -0
- package/dist/components/assets/asset-meta-drift-notice.js.map +1 -0
- package/dist/components/assets/asset-meta-label.d.ts +2 -1
- package/dist/components/assets/asset-meta-label.js +4 -2
- package/dist/components/assets/asset-meta-label.js.map +1 -1
- package/dist/components/assets/asset-metadata.d.ts +33 -13
- package/dist/components/assets/asset-metadata.js +46 -22
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/asset-search.d.ts +6 -2
- package/dist/components/assets/asset-search.js +15 -5
- package/dist/components/assets/asset-search.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +5 -3
- package/dist/components/assets/assets-collection-view-source.js +7 -5
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/builder/canvas-rebase.d.ts +36 -12
- package/dist/components/builder/canvas-rebase.js +13 -3
- package/dist/components/builder/canvas-rebase.js.map +1 -1
- package/dist/components/builder/canvas-render-context.js +5 -4
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +12 -6
- package/dist/components/builder/composition-field-editor.js +6 -5
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/cms/asset-picker-core.d.ts +2 -2
- package/dist/components/cms/asset-picker-core.js +2 -2
- package/dist/components/cms/asset-picker-core.js.map +1 -1
- package/dist/components/cms/asset-picker.js +3 -1
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/canvas-commit.d.ts +6 -0
- package/dist/components/cms/canvas-commit.js +5 -1
- package/dist/components/cms/canvas-commit.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +21 -8
- package/dist/components/cms/cms-store.js +19 -10
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +4 -11
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +2 -2
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/draft-commit.d.ts +16 -3
- package/dist/components/cms/draft-commit.js +3 -2
- package/dist/components/cms/draft-commit.js.map +1 -1
- package/dist/components/cms/use-document-draft.d.ts +67 -9
- package/dist/components/cms/use-document-draft.js +112 -39
- package/dist/components/cms/use-document-draft.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +6 -4
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +6 -4
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +7 -0
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +8 -0
- package/dist/lib/ai/tools.js +4 -0
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +15 -8
- package/dist/lib/cms-views/view-model.js +19 -5
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/collection-view/__fixtures__/document-writes.js +1 -0
- package/dist/lib/collection-view/__fixtures__/document-writes.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +15 -7
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/data/collection-content.js +5 -0
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +13 -1
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +11 -0
- package/dist/lib/i18n/messages.en.js +14 -1
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/cms-persistence.d.ts +80 -35
- package/dist/lib/operations/cms-persistence.js +348 -85
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +54 -8
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persona/labels.js +1 -0
- package/dist/lib/persona/labels.js.map +1 -1
- package/package.json +3 -3
package/convex/cms.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { v } from 'convex/values'
|
|
2
2
|
import {
|
|
3
|
+
DOCUMENT_KEY_SEPARATOR,
|
|
3
4
|
DRAFT_VARIANT,
|
|
4
5
|
applyDocumentPatches,
|
|
6
|
+
documentKey,
|
|
5
7
|
documentsSemanticEqual,
|
|
6
8
|
foldDocument,
|
|
7
9
|
foldPatchesForStorage,
|
|
@@ -26,10 +28,13 @@ import { assertWithinWriteLimits } from './write-limits'
|
|
|
26
28
|
*
|
|
27
29
|
* Owner, verbatim: "yes keep validation client-side. server-side validation enforces
|
|
28
30
|
* migrations or could lead to invalid/lost data (the payload-sql-approach). schema should
|
|
29
|
-
* be the lens and the mask to put on the data." So these handlers enforce
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
31
|
+
* be the lens and the mask to put on the data." So these handlers enforce only data-only
|
|
32
|
+
* facts: **the revision** (every write and publish carries the one it was made against; a
|
|
33
|
+
* moved document refuses with `conflict`), **pin integrity** (a pinned version exists and
|
|
34
|
+
* belongs to this document), **authority** (`requireCapability`) and — EC-649 — the
|
|
35
|
+
* **strict-reference facts the operation's guard judged** (a publish's strict targets are
|
|
36
|
+
* still published; an unpublish / delete target has no published referrer the guard did not
|
|
37
|
+
* judge): the operation decides what is strict, these handlers only re-check that data.
|
|
33
38
|
* They never validate a document against a schema and never canonicalise a value — the
|
|
34
39
|
* client gate does both (a `content.write` member bypassing the studio can publish an
|
|
35
40
|
* invalid document; delivery parse raises it visibly, AD-14).
|
|
@@ -156,6 +161,70 @@ const preconditionValidator = v.union(
|
|
|
156
161
|
v.object({ ifCollectionEmpty: v.literal(true) }),
|
|
157
162
|
)
|
|
158
163
|
|
|
164
|
+
// --- EC-649: strict-reference integrity as data preconditions (Codex F08) -------------
|
|
165
|
+
//
|
|
166
|
+
// The operation judges strictness (it holds the schema); these handlers hold none (EC-565
|
|
167
|
+
// 1e). They re-check, inside their own transaction, only the DATA the operation's guard
|
|
168
|
+
// judged: a publish / pin's strict targets are still published; an unpublish / delete's
|
|
169
|
+
// target has no published referrer the guard did not judge at its current pin. Convex OCC
|
|
170
|
+
// makes the check and the write one serializable step: a concurrent publish of a referrer
|
|
171
|
+
// writes the very `referenceEntries` range (by_target) and `cmsDocuments` rows read here.
|
|
172
|
+
|
|
173
|
+
const documentRefValidator = v.object({ collection: v.string(), id: v.string() })
|
|
174
|
+
const publishedReferrerValidator = v.object({
|
|
175
|
+
collection: v.string(),
|
|
176
|
+
id: v.string(),
|
|
177
|
+
publishedVersion: v.number(),
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
interface WireRef {
|
|
181
|
+
collection: string
|
|
182
|
+
id: string
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The strict targets that are gone or carry no pin (a publish / pin refuses on any). */
|
|
186
|
+
async function unpublishedStrictTargets(ctx: MutationCtx, targets: readonly WireRef[] | undefined): Promise<WireRef[]> {
|
|
187
|
+
const unpublished: WireRef[] = []
|
|
188
|
+
for (const target of targets ?? []) {
|
|
189
|
+
const row = await draftRow(ctx, target.collection, target.id)
|
|
190
|
+
if (row?.publishedVersion === undefined) unpublished.push({ collection: target.collection, id: target.id })
|
|
191
|
+
}
|
|
192
|
+
return unpublished
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The published referrers of `collection:docId` the guard did not judge at their current pin —
|
|
197
|
+
* read from the reference index's PUBLISHED variant (`by_target`, written from the pinned
|
|
198
|
+
* snapshot in the same transaction as every pin move — EC-286 / EC-589), so a referrer published
|
|
199
|
+
* after the guard read is seen here. `undefined` judged = not checked (a caller without the
|
|
200
|
+
* precondition).
|
|
201
|
+
*/
|
|
202
|
+
async function unjudgedPublishedReferrers(
|
|
203
|
+
ctx: MutationCtx,
|
|
204
|
+
collection: string,
|
|
205
|
+
docId: string,
|
|
206
|
+
judged: readonly (WireRef & { publishedVersion: number })[] | undefined,
|
|
207
|
+
): Promise<WireRef[]> {
|
|
208
|
+
if (!judged) return []
|
|
209
|
+
const targetKey = documentKey({ collection, id: docId })
|
|
210
|
+
const pins = new Map(judged.map((referrer) => [documentKey(referrer), referrer.publishedVersion]))
|
|
211
|
+
const entries = await ctx.db
|
|
212
|
+
.query('referenceEntries')
|
|
213
|
+
.withIndex('by_target', (q) => q.eq('targetType', 'document').eq('targetId', targetKey).eq('variant', 'published'))
|
|
214
|
+
.collect()
|
|
215
|
+
const unjudged: WireRef[] = []
|
|
216
|
+
for (const sourceId of new Set(entries.map((entry) => entry.sourceId))) {
|
|
217
|
+
if (sourceId === targetKey) continue
|
|
218
|
+
// A collection id never contains the separator (EC-578 F10), so the first one splits.
|
|
219
|
+
const at = sourceId.indexOf(DOCUMENT_KEY_SEPARATOR)
|
|
220
|
+
const source = { collection: sourceId.slice(0, at), id: sourceId.slice(at + 1) }
|
|
221
|
+
const row = await draftRow(ctx, source.collection, source.id)
|
|
222
|
+
if (row?.publishedVersion === undefined) continue
|
|
223
|
+
if (pins.get(sourceId) !== row.publishedVersion) unjudged.push(source)
|
|
224
|
+
}
|
|
225
|
+
return unjudged
|
|
226
|
+
}
|
|
227
|
+
|
|
159
228
|
/**
|
|
160
229
|
* The whole-document write (EC-595 binding point 2, EC-578): create (`ifAbsent`), a
|
|
161
230
|
* singleton's first document (`ifCollectionEmpty`), or a replace at an exact revision —
|
|
@@ -421,11 +490,35 @@ export const listDocumentPatches = query({
|
|
|
421
490
|
},
|
|
422
491
|
})
|
|
423
492
|
|
|
493
|
+
/**
|
|
494
|
+
* Delete a document with its version history, patch log and presence. EC-648: an optional
|
|
495
|
+
* `precondition: { revision }` — the delete happens only while the draft is at exactly that
|
|
496
|
+
* revision, checked in this transaction (a changeset reverting its own create carries the
|
|
497
|
+
* revision the create recorded, so a colleague's later edit refuses it). Data-only (1e).
|
|
498
|
+
* Returns `{ status: 'conflict', current }` when the precondition does not hold. EC-649: the
|
|
499
|
+
* precondition's `publishedReferrers` (the ones the operation's strict guard judged) — a
|
|
500
|
+
* published referrer not among them refuses the delete with `{ status: 'unjudged-referrers' }`.
|
|
501
|
+
*/
|
|
424
502
|
export const deleteDocument = mutation({
|
|
425
|
-
args: {
|
|
503
|
+
args: {
|
|
504
|
+
collection: v.string(),
|
|
505
|
+
docId: v.string(),
|
|
506
|
+
precondition: v.optional(
|
|
507
|
+
v.object({
|
|
508
|
+
revision: v.optional(v.number()),
|
|
509
|
+
publishedReferrers: v.optional(v.array(publishedReferrerValidator)),
|
|
510
|
+
}),
|
|
511
|
+
),
|
|
512
|
+
},
|
|
426
513
|
handler: async (ctx, args) => {
|
|
427
514
|
await requireCapability(ctx, 'content.write') // EC-150
|
|
428
515
|
const existing = await draftRow(ctx, args.collection, args.docId)
|
|
516
|
+
const revision = args.precondition?.revision
|
|
517
|
+
if (revision !== undefined && existing?.revision !== revision) {
|
|
518
|
+
return { status: 'conflict' as const, current: existing ? withUnfoldedDocument(existing) : null }
|
|
519
|
+
}
|
|
520
|
+
const unjudged = await unjudgedPublishedReferrers(ctx, args.collection, args.docId, args.precondition?.publishedReferrers)
|
|
521
|
+
if (unjudged.length > 0) return { status: 'unjudged-referrers' as const, refs: unjudged }
|
|
429
522
|
if (existing) await ctx.db.delete(existing._id)
|
|
430
523
|
// Cascade the version history (EC-224), the patch log and presence (EC-595).
|
|
431
524
|
const versions = await ctx.db
|
|
@@ -436,6 +529,7 @@ export const deleteDocument = mutation({
|
|
|
436
529
|
await deleteBookkeeping(ctx, args.collection, args.docId)
|
|
437
530
|
// EC-286: the write-chokepoint trigger drops this document's index rows (and
|
|
438
531
|
// revalidates if it was published) when the `cmsDocuments` delete commits.
|
|
532
|
+
return { status: 'ok' as const }
|
|
439
533
|
},
|
|
440
534
|
})
|
|
441
535
|
|
|
@@ -518,6 +612,8 @@ export const saveDocumentVersion = mutation({
|
|
|
518
612
|
* bump the revision and log the publish — all in this transaction; otherwise refuse with
|
|
519
613
|
* `conflict` and the current row. The `cmsDocuments` trigger sees the pin move and
|
|
520
614
|
* reindexes + revalidates (EC-286 / EC-146) — the version row it reads exists already.
|
|
615
|
+
* EC-649: `strictTargets` (the ones the operation's guard judged published) must still be
|
|
616
|
+
* published, else `{ status: 'strict-targets', refs }` and nothing is written.
|
|
521
617
|
*/
|
|
522
618
|
export const publishDocument = mutation({
|
|
523
619
|
args: {
|
|
@@ -527,6 +623,7 @@ export const publishDocument = mutation({
|
|
|
527
623
|
label: v.optional(v.string()),
|
|
528
624
|
origin: v.optional(v.string()),
|
|
529
625
|
at: v.string(),
|
|
626
|
+
strictTargets: v.optional(v.array(documentRefValidator)),
|
|
530
627
|
},
|
|
531
628
|
handler: async (ctx, args) => {
|
|
532
629
|
const actor = await requireCapability(ctx, 'content.write') // EC-150
|
|
@@ -535,6 +632,8 @@ export const publishDocument = mutation({
|
|
|
535
632
|
if (existing.revision !== args.expectedRevision) {
|
|
536
633
|
return { status: 'conflict' as const, current: withUnfoldedDocument(existing) }
|
|
537
634
|
}
|
|
635
|
+
const unpublished = await unpublishedStrictTargets(ctx, args.strictTargets)
|
|
636
|
+
if (unpublished.length > 0) return { status: 'strict-targets' as const, refs: unpublished }
|
|
538
637
|
const draft = unfoldDocument(existing.document as CmsDocument)
|
|
539
638
|
const version = await insertVersion(ctx, {
|
|
540
639
|
collection: args.collection,
|
|
@@ -652,8 +751,10 @@ export const listAllDocumentVersions = query({
|
|
|
652
751
|
* draft reads `true` — Codex P05's sibling), the revision bumps, a `pin` row is logged.
|
|
653
752
|
*
|
|
654
753
|
* Server-side trust (EC-565 1e, owner-ratified): the checks here are DATA-ONLY — revision,
|
|
655
|
-
* pin integrity, authority
|
|
656
|
-
*
|
|
754
|
+
* pin integrity, authority, and (EC-649) the strict targets the operation judged are still
|
|
755
|
+
* published (`{ status: 'strict-targets', refs }` otherwise). Schema validation stays the
|
|
756
|
+
* client gate (`documents.publish` runs it, with the hidden-clear, before `publishDocument`
|
|
757
|
+
* records the row this pins).
|
|
657
758
|
*/
|
|
658
759
|
export const publishDocumentVersion = mutation({
|
|
659
760
|
args: {
|
|
@@ -663,6 +764,7 @@ export const publishDocumentVersion = mutation({
|
|
|
663
764
|
updatedAt: v.string(),
|
|
664
765
|
// EC-447: the candidate first-publish instant, written SET-ONCE below.
|
|
665
766
|
publishedAt: v.string(),
|
|
767
|
+
strictTargets: v.optional(v.array(documentRefValidator)),
|
|
666
768
|
},
|
|
667
769
|
handler: async (ctx, args) => {
|
|
668
770
|
const actor = await requireCapability(ctx, 'content.write') // EC-150
|
|
@@ -671,6 +773,8 @@ export const publishDocumentVersion = mutation({
|
|
|
671
773
|
const target = await versionRow(ctx, args.collection, args.docId, args.version)
|
|
672
774
|
if (!target) return { status: 'missing-version' as const }
|
|
673
775
|
if (!isPinnableVersion(target)) return { status: 'not-pinnable' as const }
|
|
776
|
+
const unpublished = await unpublishedStrictTargets(ctx, args.strictTargets)
|
|
777
|
+
if (unpublished.length > 0) return { status: 'strict-targets' as const, refs: unpublished }
|
|
674
778
|
const revision = doc.revision + 1
|
|
675
779
|
const patch = {
|
|
676
780
|
revision,
|
|
@@ -692,12 +796,24 @@ export const publishDocumentVersion = mutation({
|
|
|
692
796
|
},
|
|
693
797
|
})
|
|
694
798
|
|
|
799
|
+
/**
|
|
800
|
+
* Clear the live pin (EC-224). EC-649: `publishedReferrers` — the published referrers the
|
|
801
|
+
* operation's strict guard judged; a published referrer of this document not among them (at
|
|
802
|
+
* its current pin) refuses the unpublish with `{ status: 'unjudged-referrers', refs }`.
|
|
803
|
+
*/
|
|
695
804
|
export const unpublishDocument = mutation({
|
|
696
|
-
args: {
|
|
805
|
+
args: {
|
|
806
|
+
collection: v.string(),
|
|
807
|
+
docId: v.string(),
|
|
808
|
+
updatedAt: v.string(),
|
|
809
|
+
publishedReferrers: v.optional(v.array(publishedReferrerValidator)),
|
|
810
|
+
},
|
|
697
811
|
handler: async (ctx, args) => {
|
|
698
812
|
const actor = await requireCapability(ctx, 'content.write') // EC-150
|
|
699
813
|
const doc = await draftRow(ctx, args.collection, args.docId)
|
|
700
814
|
if (!doc) return null
|
|
815
|
+
const unjudged = await unjudgedPublishedReferrers(ctx, args.collection, args.docId, args.publishedReferrers)
|
|
816
|
+
if (unjudged.length > 0) return { status: 'unjudged-referrers' as const, refs: unjudged }
|
|
701
817
|
const revision = doc.revision + 1
|
|
702
818
|
// Convex patch removes a field set to `undefined` — clears the live pin. EC-286: the
|
|
703
819
|
// trigger drops the published index rows and revalidates. EC-447/EC-449: the
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { convexTest } from 'convex-test'
|
|
3
|
+
import type { ConvexHttpClient } from 'convex/browser'
|
|
4
|
+
import { getFunctionName } from 'convex/server'
|
|
5
|
+
import type { FunctionReference } from 'convex/server'
|
|
6
|
+
import { createOperations } from '@elytracms/core/operations'
|
|
7
|
+
import { ConvexPersistenceAdapter } from '../src/lib/persistence/convex-adapter'
|
|
8
|
+
import schema from './schema'
|
|
9
|
+
|
|
10
|
+
// convex-test fires a `runAfter(0)` on a REAL timer (the publish trigger's revalidation);
|
|
11
|
+
// fake timers keep it queued.
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.useFakeTimers()
|
|
14
|
+
})
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
vi.useRealTimers()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* EC-649 — Codex F08 and its reverse, on Convex: the real `cms:*` handlers, schema and
|
|
21
|
+
* write-chokepoint triggers under convex-test, driven by the production operations over the
|
|
22
|
+
* production `ConvexPersistenceAdapter`. Each race is an explicit interleaving — the client
|
|
23
|
+
* HOLDS one operation's store mutation after its guard, runs the other operation, then sends
|
|
24
|
+
* the held mutation — and the sink REFUSES it inside its transaction. convex-test serialises
|
|
25
|
+
* mutations, so this proves the sink's data check and its refusal, not Convex's OCC retry
|
|
26
|
+
* (which is what makes the same check serializable against a truly concurrent mutation).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const modules = {
|
|
30
|
+
...import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./_generated/**']),
|
|
31
|
+
'./_generated/api.ts': async () => ({}),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const PROJECT_ID = 'strict-race'
|
|
35
|
+
const SESSION_USER_ID = 'user_admin'
|
|
36
|
+
const projectId = PROJECT_ID
|
|
37
|
+
const author = { collection: 'author', id: 'a' }
|
|
38
|
+
const post = { collection: 'post', id: 'p' }
|
|
39
|
+
const collections = () => [
|
|
40
|
+
{ id: 'author', kind: 'document' as const, fields: [{ name: 'name', type: 'text' as const }] },
|
|
41
|
+
{ id: 'post', kind: 'document' as const, fields: [{ name: 'author', type: 'relation' as const, target: 'author', multiple: false, strict: true }] },
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
/** A held store mutation: `atStore` resolves once it is held; `resume` sends it. */
|
|
45
|
+
interface Hold {
|
|
46
|
+
readonly atStore: Promise<void>
|
|
47
|
+
readonly resume: () => void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function convexWorld() {
|
|
51
|
+
const t = convexTest(schema, modules)
|
|
52
|
+
const now = new Date().toISOString()
|
|
53
|
+
await t.run(async (ctx) => {
|
|
54
|
+
await ctx.db.insert('members', { userId: SESSION_USER_ID, role: 'admin', createdAt: now, updatedAt: now })
|
|
55
|
+
})
|
|
56
|
+
const asAdmin = t.withIdentity({ subject: SESSION_USER_ID, issuer: 'https://convex.test' })
|
|
57
|
+
const holds = new Map<string, { reached: () => void; proceed: Promise<void> }>()
|
|
58
|
+
const client = {
|
|
59
|
+
query: (ref: FunctionReference<'query'>, args?: Record<string, unknown>) => asAdmin.query(ref, args ?? {}),
|
|
60
|
+
mutation: async (ref: FunctionReference<'mutation'>, args?: Record<string, unknown>) => {
|
|
61
|
+
const key = `${getFunctionName(ref)}#${String(args?.docId)}`
|
|
62
|
+
const hold = holds.get(key)
|
|
63
|
+
if (hold) {
|
|
64
|
+
holds.delete(key)
|
|
65
|
+
hold.reached()
|
|
66
|
+
await hold.proceed
|
|
67
|
+
}
|
|
68
|
+
return asAdmin.mutation(ref, args ?? {})
|
|
69
|
+
},
|
|
70
|
+
} as unknown as ConvexHttpClient
|
|
71
|
+
const adapter = new ConvexPersistenceAdapter(client, PROJECT_ID)
|
|
72
|
+
const ops = createOperations({ adapter, collections, validateDocument: () => [] })
|
|
73
|
+
/** Hold the next `cms:<mutation>` for document `id` at the client, after every guard ran. */
|
|
74
|
+
const hold = (mutation: string, id: string): Hold => {
|
|
75
|
+
let reached!: () => void
|
|
76
|
+
let resume!: () => void
|
|
77
|
+
const atStore = new Promise<void>((resolve) => { reached = resolve })
|
|
78
|
+
const proceed = new Promise<void>((resolve) => { resume = resolve })
|
|
79
|
+
holds.set(`cms:${mutation}#${id}`, { reached, proceed })
|
|
80
|
+
return { atStore, resume }
|
|
81
|
+
}
|
|
82
|
+
for (const document of [
|
|
83
|
+
{ ...author, values: { name: 'Ada' } },
|
|
84
|
+
{ ...post, values: { author: { type: 'reference', ...author } } },
|
|
85
|
+
]) expect((await ops.documents.replace({ projectId, document, precondition: { ifAbsent: true } })).ok).toBe(true)
|
|
86
|
+
return { adapter, ops, hold }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe('EC-649 — strict-reference preconditions at the Convex sink (convex-parity)', () => {
|
|
90
|
+
it('F08: the author unpublished between the post’s publish guard and its mutation — `cms:publishDocument` refuses', async () => {
|
|
91
|
+
const { adapter, ops, hold } = await convexWorld()
|
|
92
|
+
expect((await ops.documents.publish({ projectId, ref: author })).ok).toBe(true)
|
|
93
|
+
// Known-positive control: the sequential case is refused by the guard.
|
|
94
|
+
expect((await ops.documents.publish({ projectId, ref: post })).ok).toBe(true)
|
|
95
|
+
const blocked = await ops.documents.unpublish({ projectId, ref: author })
|
|
96
|
+
expect(!blocked.ok && blocked.error.code).toBe('conflict')
|
|
97
|
+
expect((await ops.documents.unpublish({ projectId, ref: post })).ok).toBe(true)
|
|
98
|
+
|
|
99
|
+
const held = hold('publishDocument', post.id)
|
|
100
|
+
const publishing = ops.documents.publish({ projectId, ref: post })
|
|
101
|
+
await held.atStore
|
|
102
|
+
expect((await ops.documents.unpublish({ projectId, ref: author })).ok).toBe(true)
|
|
103
|
+
held.resume()
|
|
104
|
+
const published = await publishing
|
|
105
|
+
expect(published.ok).toBe(false)
|
|
106
|
+
if (!published.ok) expect(published.error.messageRef?.key).toBe('operations.error.strictTargetsUnpublishedOne')
|
|
107
|
+
expect((await adapter.cms.getDocument(projectId, author)).publishedVersion).toBeUndefined()
|
|
108
|
+
expect(await adapter.cms.listPublishedDocuments(projectId, 'post')).toEqual([])
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('the reverse: the post published between the author’s unpublish guard and its mutation — `cms:unpublishDocument` refuses', async () => {
|
|
112
|
+
const { adapter, ops, hold } = await convexWorld()
|
|
113
|
+
expect((await ops.documents.publish({ projectId, ref: author })).ok).toBe(true)
|
|
114
|
+
const held = hold('unpublishDocument', author.id)
|
|
115
|
+
const unpublishing = ops.documents.unpublish({ projectId, ref: author })
|
|
116
|
+
await held.atStore // the guard found no published strict referrer
|
|
117
|
+
expect((await ops.documents.publish({ projectId, ref: post })).ok).toBe(true)
|
|
118
|
+
held.resume()
|
|
119
|
+
const unpublished = await unpublishing
|
|
120
|
+
expect(unpublished.ok).toBe(false)
|
|
121
|
+
if (!unpublished.ok) {
|
|
122
|
+
expect(unpublished.error.code).toBe('conflict')
|
|
123
|
+
expect(unpublished.error.messageRef?.key).toBe('operations.error.strictReferencesBlockUnpublishOne')
|
|
124
|
+
}
|
|
125
|
+
expect((await adapter.cms.getDocument(projectId, author)).publishedVersion).toBe(1)
|
|
126
|
+
expect((await adapter.cms.getDocument(projectId, post)).publishedVersion).toBe(1)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('control: the held unpublish with nothing racing it lands (the sink reads the index, finds no unjudged referrer)', async () => {
|
|
130
|
+
const { adapter, ops, hold } = await convexWorld()
|
|
131
|
+
expect((await ops.documents.publish({ projectId, ref: author })).ok).toBe(true)
|
|
132
|
+
const held = hold('unpublishDocument', author.id)
|
|
133
|
+
const unpublishing = ops.documents.unpublish({ projectId, ref: author })
|
|
134
|
+
await held.atStore
|
|
135
|
+
held.resume()
|
|
136
|
+
expect((await unpublishing).ok).toBe(true)
|
|
137
|
+
expect((await adapter.cms.getDocument(projectId, author)).publishedVersion).toBeUndefined()
|
|
138
|
+
})
|
|
139
|
+
})
|
|
@@ -66,6 +66,12 @@ export function AiChatPanel() {
|
|
|
66
66
|
...doc.values,
|
|
67
67
|
...(doc.localized?.[activeLocale] ?? {}),
|
|
68
68
|
}), [activeLocale]);
|
|
69
|
+
/**
|
|
70
|
+
* EC-647 — the revision each document the assistant READ was at, keyed by the value the
|
|
71
|
+
* reader returned (the token the proposal is built from): an update / translation
|
|
72
|
+
* proposal records it, and its apply writes at it.
|
|
73
|
+
*/
|
|
74
|
+
const readRevisions = useRef(new WeakMap());
|
|
69
75
|
/** Operations-backed tool readers — client-side, with the user's own session. */
|
|
70
76
|
const readers = useMemo(() => {
|
|
71
77
|
const matches = (query, ...fields) => !query || fields.some((f) => f?.toLowerCase().includes(query.toLowerCase()));
|
|
@@ -118,8 +124,12 @@ export function AiChatPanel() {
|
|
|
118
124
|
await cms.flushPersistence({ collection, id });
|
|
119
125
|
const { operations } = await getStudioOperations();
|
|
120
126
|
const result = await operations.documents.get({ projectId, ref: { collection, id } });
|
|
121
|
-
|
|
127
|
+
if (!result.ok || !result.value)
|
|
128
|
+
return null;
|
|
129
|
+
readRevisions.current.set(result.value.document, result.value.revision);
|
|
130
|
+
return result.value.document;
|
|
122
131
|
},
|
|
132
|
+
revisionOf: (document) => readRevisions.current.get(document),
|
|
123
133
|
};
|
|
124
134
|
}, [projectId, cms, localeValues]);
|
|
125
135
|
/** EC-578: a fresh document id from the session's injected id factory (UUIDs). */
|
|
@@ -338,7 +348,10 @@ export function AiChatPanel() {
|
|
|
338
348
|
if (current && current.collection === ref.collection && current.id === ref.id)
|
|
339
349
|
await current.flush();
|
|
340
350
|
if (existing && proposal.patches) {
|
|
341
|
-
|
|
351
|
+
// EC-647: AT the revision of the read the proposal was built from — a position
|
|
352
|
+
// changed since then by anyone else is refused (the apply fails visibly), never
|
|
353
|
+
// overwritten by content the assistant never saw.
|
|
354
|
+
const outcome = await cms.commitPatches(ref, proposal.patches, proposal.patches.map(() => proposal.revision));
|
|
342
355
|
if (outcome?.status === 'failed' || outcome?.status === 'refused') {
|
|
343
356
|
dispatch({ type: 'fail', message: translate('ai.error.applyFailed') });
|
|
344
357
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-chat-panel.js","sourceRoot":"","sources":["../../../src/components/ai/ai-chat-panel.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useReducer, useRef, useState } from 'react'\nimport { MessageSquarePlus, X } from 'lucide-react'\nimport { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from 'ai'\nimport type { UIMessage } from 'ai'\nimport { useChat } from '@ai-sdk/react'\nimport {\n createCollectionRegistry,\n resolveDocumentTitle,\n} from '@elytracms/core/cms-core'\nimport type { CmsDocument, FieldValues } from '@elytracms/core/cms-core'\nimport { Button, Heading, Row, Spacer, Text } from '@elytracms/studio/ui'\nimport { translate } from '../../lib/i18n'\nimport { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router'\nimport { resolveDeploymentAi } from '../../lib/workspace'\nimport { getStudioOperations } from '../../lib/operations'\nimport {\n DEFAULT_MAX_STEPS,\n buildChatContext,\n chatReviewReducer,\n clearConversation,\n createChatCandidateStore,\n createChatToolExecutor,\n guardAiRouteFetch,\n initialChatReviewState,\n introducedErrors,\n isAiRouteNotMountedError,\n renderCurrentDocumentSection,\n resolveAiConfig,\n resolveChatTarget,\n resolveKnowledge,\n withStepCap,\n} from '../../lib/ai'\nimport { getAiCurrentDocument } from '../../lib/ai-current-document'\nimport type {\n ChatCandidateStore,\n ChatToolReaders,\n KnowledgeCollectionReader,\n ProposalReview,\n ResolvedKnowledge,\n RouteHeaders,\n} from '../../lib/ai'\nimport { useCms } from '../cms/cms-store'\nimport {\n AssistantTurn,\n ChatErrorNote,\n Conversation,\n ConversationEmpty,\n PromptInput,\n ToolStatus,\n UserTurn,\n WorkingIndicator,\n describeToolPart,\n messageText,\n segmentMessageParts,\n} from './chat-ui'\nimport type { MessagePartView } from './chat-ui'\nimport { ProposalCard } from './proposal-card'\nimport { setAiPanelOpen } from '../../lib/ai-panel'\n\n/**\n * The AI assistant chat panel (EC-199, moved onto the SDK's native protocol by\n * EC-334) — the right-hand collapsible surface in the CMS area. `useChat` +\n * `DefaultChatTransport` own the conversation against the route's native chat\n * branch (visible token streaming); `onToolCall` dispatches into the client\n * executor map (operations reads + terminal engines, all with the user's own\n * session); `sendAutomaticallyWhen` is the SDK predicate wrapped with the\n * per-turn step cap. What the SDK does not own lives in the pure review reducer:\n * full proposals keyed by `toolCallId` (reviewed as a diff, applied as a DRAFT\n * via operations upsert + a provenance-tagged snapshot — the EC-199 trust\n * boundary, unmoved) and blocking errors. EC-380: the \"new conversation\"\n * affordance clears busy-safely (stop → new chat id + review reset; pending\n * proposals are discarded with reject semantics).\n *\n * NEVER a modal (house rule). The whole module (including `ai` + `@ai-sdk/react`)\n * sits behind the lazy boundary (`ai-chat-panel-lazy`) — pinned in bundle tests.\n */\n\n/** Adapt a `RouteHeaders` (static or thunk) to the transport's headers resolvable. */\nfunction toTransportHeaders(headers: RouteHeaders | undefined): () => Promise<Record<string, string>> {\n return async () => {\n const resolved = typeof headers === 'function' ? await headers() : headers\n const record: Record<string, string> = {}\n new Headers(resolved ?? {}).forEach((value, key) => {\n record[key] = value\n })\n return record\n }\n}\n\nexport function AiChatPanel() {\n const cms = useCms()\n const navigate = useNavigate()\n const [review, dispatch] = useReducer(chatReviewReducer, initialChatReviewState)\n const [input, setInput] = useState('')\n // EC-380: the conversation counter — bumping it mints a fresh `useChat` id\n // (a brand-new conversation) and a fresh relation-candidate store.\n const [conversation, setConversation] = useState(1)\n\n const projectId = cms.projectId\n const activeLocale = cms.activeLocale\n\n // Resolve the AI platform config once (presence + capability gate). The panel only\n // mounts when configured, but a misconfigured provider still fails loud here.\n const resolved = useMemo(() => resolveAiConfig(resolveDeploymentAi()), [])\n const provider = resolved.resolved?.provider ?? null\n const knowledgeSources = useMemo(() => resolved.resolved?.knowledge ?? [], [resolved])\n const maxRepairAttempts = resolved.resolved?.limits?.maxRepairAttempts\n const ready = provider !== null && resolved.ok && projectId !== undefined\n\n const registry = useMemo(() => createCollectionRegistry(cms.collections), [cms.collections])\n\n /** Merge a document's non-localized + active-locale values for title/prose reads. */\n const localeValues = useCallback(\n (doc: CmsDocument): FieldValues => ({\n ...doc.values,\n ...(doc.localized?.[activeLocale] ?? {}),\n }),\n [activeLocale],\n )\n\n /** Operations-backed tool readers — client-side, with the user's own session. */\n const readers = useMemo<ChatToolReaders>(() => {\n const matches = (query: string | undefined, ...fields: (string | undefined)[]) =>\n !query || fields.some((f) => f?.toLowerCase().includes(query.toLowerCase()))\n return {\n async searchDocuments({ collection, query }) {\n if (projectId === undefined) return []\n const { operations } = await getStudioOperations()\n const result = await operations.documents.list({ projectId, collection })\n if (!result.ok) return []\n const target = cms.getCollection(collection)\n return result.value\n .map((rec) => ({\n id: rec.document.id,\n title: target\n ? resolveDocumentTitle(target, localeValues(rec.document), rec.document.id)\n : rec.document.id,\n }))\n .filter((r) => matches(query, r.title, r.id))\n },\n async listAssets({ query }) {\n if (projectId === undefined) return []\n const { operations } = await getStudioOperations()\n const result = await operations.assets.list({ projectId })\n if (!result.ok) return []\n // EC-588: the type facts ride along, so each asset field's candidates are\n // filtered by its `accept` (and the model sees what each asset is).\n return result.value\n .map((asset) => ({\n id: asset.id,\n title: asset.title ?? asset.filename,\n contentType: asset.contentType,\n filename: asset.filename,\n }))\n .filter((r) => matches(query, r.title, r.id))\n },\n // Full document (draft values + localized) — the getDocument/translate tools.\n async getDocument({ collection, id }) {\n if (projectId === undefined) return null\n // EC-595 A2: the FORCED FLUSH before the assistant reads a document — the open\n // editor's uncommitted fields are committed and every queued write lands, so the\n // proposal is built on the committed draft.\n const current = getAiCurrentDocument()\n if (current && current.collection === collection && current.id === id) await current.flush()\n await cms.flushPersistence({ collection, id })\n const { operations } = await getStudioOperations()\n const result = await operations.documents.get({ projectId, ref: { collection, id } })\n return result.ok ? result.value?.document ?? null : null\n },\n }\n }, [projectId, cms, localeValues])\n\n /** EC-578: a fresh document id from the session's injected id factory (UUIDs). */\n const mintDocumentId = useCallback((_collectionId: string): string => cms.newDocumentId(), [cms])\n\n /** Read `fromCollection(...)` knowledge prose through the operations layer. */\n const readCollection = useCallback<KnowledgeCollectionReader>(\n async (source) => {\n if (projectId === undefined) return []\n const { operations } = await getStudioOperations()\n const result = await operations.documents.list({ projectId, collection: source.collectionId })\n if (!result.ok) return []\n const target = cms.getCollection(source.collectionId)\n const field =\n source.field ?? target?.fields.find((f) => f.type === 'text' || f.type === 'richText')?.name\n return result.value\n .map((rec) => {\n const values = localeValues(rec.document)\n const content = field ? String(values[field] ?? '') : ''\n return { content }\n })\n .filter((item) => item.content.trim().length > 0)\n },\n [projectId, cms, localeValues],\n )\n\n // The knowledge snapshot the executors read — refreshed by the body thunk on\n // every request, so the terminal engines see what the model saw this turn.\n const knowledgeRef = useRef<ResolvedKnowledge>({ items: [] })\n\n // The chat transport target: the configured route provider's endpoint +\n // session-header thunk (EC-371), re-evaluated per request.\n const chatTarget = useMemo(() => resolveChatTarget(provider), [provider])\n\n const transport = useMemo(\n () =>\n new DefaultChatTransport({\n api: chatTarget.endpoint,\n headers: toTransportHeaders(chatTarget.headers),\n // EC-514: the same response-shape guard the structured proxy uses. Without\n // it a 200 HTML shell (route not mounted — the EC-503 dev degrade) parses\n // as an EMPTY event stream and the turn ends silently with no error at all.\n fetch: guardAiRouteFetch(),\n // The `body` THUNK — re-evaluated per request (fresh context every turn,\n // continuations included): collections-catalog summary + client-resolved\n // knowledge + EC-378's current-document block (read from the seam store\n // at REQUEST time, so every turn sees what is open NOW). The route folds\n // these sections into the system prompt; it resolves no data itself\n // (zero-data-access proxy). The block is advisory — the deterministic\n // omitted-target fallback lives in the executor.\n body: async () => {\n const knowledge = await resolveKnowledge(knowledgeSources, { readCollection })\n knowledgeRef.current = knowledge\n const current = getAiCurrentDocument()\n return {\n context: buildChatContext({\n collections: cms.collections,\n knowledge,\n ...(current\n ? {\n extraSections: [\n renderCurrentDocumentSection({\n collection: current.collection,\n id: current.id,\n title: current.title,\n locale: activeLocale,\n }),\n ],\n }\n : {}),\n }),\n }\n },\n }),\n [chatTarget, knowledgeSources, readCollection, cms.collections, activeLocale],\n )\n\n // Per-conversation relation-candidate store (EC-380: replaced on clear, so a\n // fresh conversation cannot reference the previous one's searched ids).\n const candidatesRef = useRef<{ conversation: number; store: ChatCandidateStore }>({\n conversation,\n store: createChatCandidateStore(),\n })\n if (candidatesRef.current.conversation !== conversation) {\n candidatesRef.current = { conversation, store: createChatCandidateStore() }\n }\n const candidateStore = candidatesRef.current.store\n\n const executor = useMemo(() => {\n if (!provider) return null\n return createChatToolExecutor({\n provider,\n registry,\n collections: cms.collections,\n readers,\n mintDocumentId,\n candidates: candidateStore,\n onProposal: (toolCallId, proposal) => dispatch({ type: 'propose', toolCallId, proposal }),\n knowledge: () => knowledgeRef.current,\n // EC-378: the current-document seam, read at EXECUTION time — the\n // deterministic fallback when the model omits a tool's target.\n currentDocument: () => getAiCurrentDocument(),\n locale: activeLocale,\n ...(cms.localeConfig ? { localeConfig: cms.localeConfig } : {}),\n // EC-473 established that the assistant must validate with the EDITOR's\n // rules, not a subset. EC-497 hands over THE bag itself — one value, built\n // once by `gateValidateOptions` — so the assistant's gate and the editor's\n // gate cannot drift by someone forgetting a key at a call site.\n validateOptions: cms.validateOptions,\n ...(maxRepairAttempts !== undefined ? { maxRepairAttempts } : {}),\n })\n }, [\n provider,\n registry,\n cms.collections,\n cms.localeConfig,\n cms.validateOptions,\n readers,\n mintDocumentId,\n activeLocale,\n maxRepairAttempts,\n candidateStore,\n ])\n\n const chat = useChat({\n // A new conversation is a new chat id — `useChat` recreates its state.\n id: `elytra-assistant-${conversation}`,\n transport,\n onToolCall: async ({ toolCall }) => {\n if (!executor) return\n const outcome = await executor.execute({\n toolCallId: toolCall.toolCallId,\n toolName: toolCall.toolName,\n input: toolCall.input,\n })\n // Deliberately NOT awaited: `onToolCall` runs inside the chat's serial\n // stream job, and `addToolOutput` enqueues on the same executor —\n // awaiting it here deadlocks the stream (verified against ai@7.0.22).\n if (outcome.ok) {\n void chat.addToolOutput({\n tool: toolCall.toolName,\n toolCallId: toolCall.toolCallId,\n output: outcome.output,\n })\n } else {\n void chat.addToolOutput({\n state: 'output-error',\n tool: toolCall.toolName,\n toolCallId: toolCall.toolCallId,\n errorText: outcome.errorText,\n })\n }\n },\n // The SDK predicate wrapped with the per-user-turn step cap: a capped turn\n // stops cleanly with a visible note — never a silent stall (EC-334).\n sendAutomaticallyWhen: withStepCap(lastAssistantMessageIsCompleteWithToolCalls, {\n maxSteps: DEFAULT_MAX_STEPS,\n onCap: () =>\n dispatch({ type: 'fail', message: translate('ai.error.stepCap', { max: DEFAULT_MAX_STEPS }) }),\n }),\n // EC-514: \"no route mounted at <path>\" is named as itself; every other\n // transport/provider failure keeps the generic route note.\n onError: (error) =>\n dispatch({\n type: 'fail',\n message: isAiRouteNotMountedError(error)\n ? translate('ai.error.routeNotMounted', { endpoint: error.endpoint })\n : translate('ai.error.route'),\n }),\n })\n\n const busy = chat.status === 'submitted' || chat.status === 'streaming'\n\n const submit = useCallback(() => {\n const text = input.trim()\n if (text.length === 0 || busy || !ready) return\n setInput('')\n dispatch({ type: 'clear-error' })\n void chat.sendMessage({ text })\n }, [input, busy, ready, chat])\n\n // EC-380: clear / new topic — busy-safe (stop first), pending proposals\n // discarded with reject semantics (the review state resets; nothing was applied).\n const startNewConversation = useCallback(() => {\n void clearConversation({ status: chat.status, stop: chat.stop }, () => {\n dispatch({ type: 'clear' })\n setInput('')\n setConversation((current) => current + 1)\n })\n }, [chat.status, chat.stop])\n\n const applyProposal = useCallback(\n async (proposal: ProposalReview) => {\n if (projectId === undefined) return\n // For an update/translate of an EXISTING document, spread the stored doc\n // first so document-level extras (the alias) survive the apply —\n // the patch replaces only values/localized.\n const existing = cms.getDocument({ collection: proposal.collection, id: proposal.documentId })\n const doc: CmsDocument = {\n ...(existing ?? {}),\n collection: proposal.collection,\n id: proposal.documentId,\n values: proposal.patch.values,\n ...(proposal.patch.localized ? { localized: proposal.patch.localized } : {}),\n }\n // EC-473 — re-validate BEFORE the write. The proposal was validated when it was\n // built, but the editor may have changed the document since (or applied an\n // earlier proposal on top of it), so the patch is re-judged against what is in\n // the session NOW. Same gate as every other AI write path: only errors the\n // apply would INTRODUCE block it — pre-existing brokenness on the document is\n // not the assistant's doing and must not make an unrelated change unappliable.\n const introduced = introducedErrors(\n existing ? cms.validate(existing) : [],\n cms.validate(doc),\n )\n if (introduced.length > 0) {\n dispatch({\n type: 'fail',\n message: translate('ai.error.applyInvalid', {\n issues: introduced\n .slice(0, 3)\n .map((issue) => issue.message)\n .join('; '),\n }),\n })\n return\n }\n // EC-595 A2: an update / translation persists the POSITIONS it changes (the\n // proposal's patches) — never the merged whole document, which would revert any\n // change made since the proposal was built; a create is insert-if-absent. The\n // FORCED FLUSH first, so the open editor's uncommitted fields land before.\n const current = getAiCurrentDocument()\n const ref = { collection: proposal.collection, id: proposal.documentId }\n if (current && current.collection === ref.collection && current.id === ref.id) await current.flush()\n if (existing && proposal.patches) {\n const outcome = await cms.commitPatches(ref, proposal.patches)\n if (outcome?.status === 'failed' || outcome?.status === 'refused') {\n dispatch({ type: 'fail', message: translate('ai.error.applyFailed') })\n return\n }\n } else if (!existing) {\n cms.insertDocument(doc)\n await cms.flushPersistence(ref)\n } else {\n // An update without positions cannot be applied without overwriting — say so.\n dispatch({ type: 'fail', message: translate('ai.error.applyFailed') })\n return\n }\n const { operations } = await getStudioOperations()\n // Provenance: record an AI-tagged version snapshot (a restore point + the\n // 'assistant' origin surfaced in the versions pane). Best-effort — the draft\n // already landed above; a snapshot failure must not lose the applied content.\n try {\n await operations.documents.snapshot({\n projectId,\n ref: { collection: proposal.collection, id: proposal.documentId },\n label: translate('cms.version.aiAuthoredTitle'),\n origin: 'assistant',\n })\n } catch {\n // ignore — provenance is additive\n }\n // The session already shows the applied positions (the persister's view), and an\n // open editor rebases on it — its own uncommitted fields win (EC-595 point 7).\n dispatch({ type: 'apply', toolCallId: proposal.id })\n },\n [projectId, cms],\n )\n\n const openProposal = useCallback(\n (proposal: ProposalReview) => {\n if (projectId === undefined) return\n void navigate({\n to: STUDIO_PROJECT_ROUTE,\n params: { projectId, _splat: studioPath.cmsDocument(proposal.collection, proposal.documentId) },\n })\n },\n [navigate, projectId],\n )\n\n const hasProposal = useCallback(\n (toolCallId: string) => review.proposals[toolCallId] !== undefined,\n [review.proposals],\n )\n\n const renderAssistantMessage = (message: UIMessage) => {\n const segments = segmentMessageParts(message.parts as MessagePartView[], hasProposal)\n return segments.map((segment, index) => {\n if (segment.kind === 'text') {\n return (\n <AssistantTurn key={`${message.id}-${index}`}>\n <Text size=\"sm\" className=\"whitespace-pre-wrap break-words\">\n {segment.text}\n </Text>\n </AssistantTurn>\n )\n }\n if (segment.kind === 'tools') {\n return (\n <AssistantTurn key={`${message.id}-${index}`}>\n <ToolStatus steps={segment.parts.map(describeToolPart)} />\n </AssistantTurn>\n )\n }\n const proposal = review.proposals[segment.toolCallId]\n if (!proposal) return null\n return (\n <ProposalCard\n key={segment.toolCallId}\n proposal={proposal}\n onApply={() => void applyProposal(proposal)}\n onDiscard={() => dispatch({ type: 'reject', toolCallId: proposal.id })}\n onOpen={() => openProposal(proposal)}\n />\n )\n })\n }\n\n return (\n <aside className=\"flex h-full min-h-0 w-[360px] shrink-0 flex-col border-l border-border bg-card\">\n <Row gap=\"2\" align=\"center\" className=\"h-12 shrink-0 border-b border-border px-3\">\n <Heading level={2} size=\"h4\">\n {translate('ai.panel.title')}\n </Heading>\n <Spacer />\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n onClick={startNewConversation}\n disabled={chat.messages.length === 0 && review.error === null && !busy}\n title={translate('ai.panel.newConversation')}\n aria-label={translate('ai.panel.newConversation')}\n >\n <MessageSquarePlus />\n </Button>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n onClick={() => setAiPanelOpen(false)}\n title={translate('ai.panel.close')}\n aria-label={translate('ai.panel.close')}\n >\n <X />\n </Button>\n </Row>\n\n <Conversation atBottomKey={chat.messages}>\n {chat.messages.length === 0 ? <ConversationEmpty /> : null}\n {chat.messages.map((message) =>\n message.role === 'user' ? (\n <UserTurn key={message.id} text={messageText(message)} />\n ) : (\n <div key={message.id} className=\"contents\">\n {renderAssistantMessage(message)}\n </div>\n ),\n )}\n {chat.status === 'submitted' ? (\n <AssistantTurn>\n <WorkingIndicator />\n </AssistantTurn>\n ) : null}\n {review.error !== null ? (\n <AssistantTurn>\n <ChatErrorNote message={review.error} />\n </AssistantTurn>\n ) : null}\n </Conversation>\n\n {ready ? (\n <PromptInput value={input} onChange={setInput} onSubmit={submit} disabled={busy} />\n ) : (\n <div className=\"border-t border-border p-3\">\n <Text size=\"xs\" tone=\"muted\">\n {translate('ai.panel.unavailable')}\n </Text>\n </div>\n )}\n </aside>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ai-chat-panel.js","sourceRoot":"","sources":["../../../src/components/ai/ai-chat-panel.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useReducer, useRef, useState } from 'react'\nimport { MessageSquarePlus, X } from 'lucide-react'\nimport { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from 'ai'\nimport type { UIMessage } from 'ai'\nimport { useChat } from '@ai-sdk/react'\nimport {\n createCollectionRegistry,\n resolveDocumentTitle,\n} from '@elytracms/core/cms-core'\nimport type { CmsDocument, FieldValues } from '@elytracms/core/cms-core'\nimport { Button, Heading, Row, Spacer, Text } from '@elytracms/studio/ui'\nimport { translate } from '../../lib/i18n'\nimport { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router'\nimport { resolveDeploymentAi } from '../../lib/workspace'\nimport { getStudioOperations } from '../../lib/operations'\nimport {\n DEFAULT_MAX_STEPS,\n buildChatContext,\n chatReviewReducer,\n clearConversation,\n createChatCandidateStore,\n createChatToolExecutor,\n guardAiRouteFetch,\n initialChatReviewState,\n introducedErrors,\n isAiRouteNotMountedError,\n renderCurrentDocumentSection,\n resolveAiConfig,\n resolveChatTarget,\n resolveKnowledge,\n withStepCap,\n} from '../../lib/ai'\nimport { getAiCurrentDocument } from '../../lib/ai-current-document'\nimport type {\n ChatCandidateStore,\n ChatToolReaders,\n KnowledgeCollectionReader,\n ProposalReview,\n ResolvedKnowledge,\n RouteHeaders,\n} from '../../lib/ai'\nimport { useCms } from '../cms/cms-store'\nimport {\n AssistantTurn,\n ChatErrorNote,\n Conversation,\n ConversationEmpty,\n PromptInput,\n ToolStatus,\n UserTurn,\n WorkingIndicator,\n describeToolPart,\n messageText,\n segmentMessageParts,\n} from './chat-ui'\nimport type { MessagePartView } from './chat-ui'\nimport { ProposalCard } from './proposal-card'\nimport { setAiPanelOpen } from '../../lib/ai-panel'\n\n/**\n * The AI assistant chat panel (EC-199, moved onto the SDK's native protocol by\n * EC-334) — the right-hand collapsible surface in the CMS area. `useChat` +\n * `DefaultChatTransport` own the conversation against the route's native chat\n * branch (visible token streaming); `onToolCall` dispatches into the client\n * executor map (operations reads + terminal engines, all with the user's own\n * session); `sendAutomaticallyWhen` is the SDK predicate wrapped with the\n * per-turn step cap. What the SDK does not own lives in the pure review reducer:\n * full proposals keyed by `toolCallId` (reviewed as a diff, applied as a DRAFT\n * via operations upsert + a provenance-tagged snapshot — the EC-199 trust\n * boundary, unmoved) and blocking errors. EC-380: the \"new conversation\"\n * affordance clears busy-safely (stop → new chat id + review reset; pending\n * proposals are discarded with reject semantics).\n *\n * NEVER a modal (house rule). The whole module (including `ai` + `@ai-sdk/react`)\n * sits behind the lazy boundary (`ai-chat-panel-lazy`) — pinned in bundle tests.\n */\n\n/** Adapt a `RouteHeaders` (static or thunk) to the transport's headers resolvable. */\nfunction toTransportHeaders(headers: RouteHeaders | undefined): () => Promise<Record<string, string>> {\n return async () => {\n const resolved = typeof headers === 'function' ? await headers() : headers\n const record: Record<string, string> = {}\n new Headers(resolved ?? {}).forEach((value, key) => {\n record[key] = value\n })\n return record\n }\n}\n\nexport function AiChatPanel() {\n const cms = useCms()\n const navigate = useNavigate()\n const [review, dispatch] = useReducer(chatReviewReducer, initialChatReviewState)\n const [input, setInput] = useState('')\n // EC-380: the conversation counter — bumping it mints a fresh `useChat` id\n // (a brand-new conversation) and a fresh relation-candidate store.\n const [conversation, setConversation] = useState(1)\n\n const projectId = cms.projectId\n const activeLocale = cms.activeLocale\n\n // Resolve the AI platform config once (presence + capability gate). The panel only\n // mounts when configured, but a misconfigured provider still fails loud here.\n const resolved = useMemo(() => resolveAiConfig(resolveDeploymentAi()), [])\n const provider = resolved.resolved?.provider ?? null\n const knowledgeSources = useMemo(() => resolved.resolved?.knowledge ?? [], [resolved])\n const maxRepairAttempts = resolved.resolved?.limits?.maxRepairAttempts\n const ready = provider !== null && resolved.ok && projectId !== undefined\n\n const registry = useMemo(() => createCollectionRegistry(cms.collections), [cms.collections])\n\n /** Merge a document's non-localized + active-locale values for title/prose reads. */\n const localeValues = useCallback(\n (doc: CmsDocument): FieldValues => ({\n ...doc.values,\n ...(doc.localized?.[activeLocale] ?? {}),\n }),\n [activeLocale],\n )\n\n /**\n * EC-647 — the revision each document the assistant READ was at, keyed by the value the\n * reader returned (the token the proposal is built from): an update / translation\n * proposal records it, and its apply writes at it.\n */\n const readRevisions = useRef(new WeakMap<CmsDocument, number>())\n\n /** Operations-backed tool readers — client-side, with the user's own session. */\n const readers = useMemo<ChatToolReaders>(() => {\n const matches = (query: string | undefined, ...fields: (string | undefined)[]) =>\n !query || fields.some((f) => f?.toLowerCase().includes(query.toLowerCase()))\n return {\n async searchDocuments({ collection, query }) {\n if (projectId === undefined) return []\n const { operations } = await getStudioOperations()\n const result = await operations.documents.list({ projectId, collection })\n if (!result.ok) return []\n const target = cms.getCollection(collection)\n return result.value\n .map((rec) => ({\n id: rec.document.id,\n title: target\n ? resolveDocumentTitle(target, localeValues(rec.document), rec.document.id)\n : rec.document.id,\n }))\n .filter((r) => matches(query, r.title, r.id))\n },\n async listAssets({ query }) {\n if (projectId === undefined) return []\n const { operations } = await getStudioOperations()\n const result = await operations.assets.list({ projectId })\n if (!result.ok) return []\n // EC-588: the type facts ride along, so each asset field's candidates are\n // filtered by its `accept` (and the model sees what each asset is).\n return result.value\n .map((asset) => ({\n id: asset.id,\n title: asset.title ?? asset.filename,\n contentType: asset.contentType,\n filename: asset.filename,\n }))\n .filter((r) => matches(query, r.title, r.id))\n },\n // Full document (draft values + localized) — the getDocument/translate tools.\n async getDocument({ collection, id }) {\n if (projectId === undefined) return null\n // EC-595 A2: the FORCED FLUSH before the assistant reads a document — the open\n // editor's uncommitted fields are committed and every queued write lands, so the\n // proposal is built on the committed draft.\n const current = getAiCurrentDocument()\n if (current && current.collection === collection && current.id === id) await current.flush()\n await cms.flushPersistence({ collection, id })\n const { operations } = await getStudioOperations()\n const result = await operations.documents.get({ projectId, ref: { collection, id } })\n if (!result.ok || !result.value) return null\n readRevisions.current.set(result.value.document, result.value.revision)\n return result.value.document\n },\n revisionOf: (document) => readRevisions.current.get(document),\n }\n }, [projectId, cms, localeValues])\n\n /** EC-578: a fresh document id from the session's injected id factory (UUIDs). */\n const mintDocumentId = useCallback((_collectionId: string): string => cms.newDocumentId(), [cms])\n\n /** Read `fromCollection(...)` knowledge prose through the operations layer. */\n const readCollection = useCallback<KnowledgeCollectionReader>(\n async (source) => {\n if (projectId === undefined) return []\n const { operations } = await getStudioOperations()\n const result = await operations.documents.list({ projectId, collection: source.collectionId })\n if (!result.ok) return []\n const target = cms.getCollection(source.collectionId)\n const field =\n source.field ?? target?.fields.find((f) => f.type === 'text' || f.type === 'richText')?.name\n return result.value\n .map((rec) => {\n const values = localeValues(rec.document)\n const content = field ? String(values[field] ?? '') : ''\n return { content }\n })\n .filter((item) => item.content.trim().length > 0)\n },\n [projectId, cms, localeValues],\n )\n\n // The knowledge snapshot the executors read — refreshed by the body thunk on\n // every request, so the terminal engines see what the model saw this turn.\n const knowledgeRef = useRef<ResolvedKnowledge>({ items: [] })\n\n // The chat transport target: the configured route provider's endpoint +\n // session-header thunk (EC-371), re-evaluated per request.\n const chatTarget = useMemo(() => resolveChatTarget(provider), [provider])\n\n const transport = useMemo(\n () =>\n new DefaultChatTransport({\n api: chatTarget.endpoint,\n headers: toTransportHeaders(chatTarget.headers),\n // EC-514: the same response-shape guard the structured proxy uses. Without\n // it a 200 HTML shell (route not mounted — the EC-503 dev degrade) parses\n // as an EMPTY event stream and the turn ends silently with no error at all.\n fetch: guardAiRouteFetch(),\n // The `body` THUNK — re-evaluated per request (fresh context every turn,\n // continuations included): collections-catalog summary + client-resolved\n // knowledge + EC-378's current-document block (read from the seam store\n // at REQUEST time, so every turn sees what is open NOW). The route folds\n // these sections into the system prompt; it resolves no data itself\n // (zero-data-access proxy). The block is advisory — the deterministic\n // omitted-target fallback lives in the executor.\n body: async () => {\n const knowledge = await resolveKnowledge(knowledgeSources, { readCollection })\n knowledgeRef.current = knowledge\n const current = getAiCurrentDocument()\n return {\n context: buildChatContext({\n collections: cms.collections,\n knowledge,\n ...(current\n ? {\n extraSections: [\n renderCurrentDocumentSection({\n collection: current.collection,\n id: current.id,\n title: current.title,\n locale: activeLocale,\n }),\n ],\n }\n : {}),\n }),\n }\n },\n }),\n [chatTarget, knowledgeSources, readCollection, cms.collections, activeLocale],\n )\n\n // Per-conversation relation-candidate store (EC-380: replaced on clear, so a\n // fresh conversation cannot reference the previous one's searched ids).\n const candidatesRef = useRef<{ conversation: number; store: ChatCandidateStore }>({\n conversation,\n store: createChatCandidateStore(),\n })\n if (candidatesRef.current.conversation !== conversation) {\n candidatesRef.current = { conversation, store: createChatCandidateStore() }\n }\n const candidateStore = candidatesRef.current.store\n\n const executor = useMemo(() => {\n if (!provider) return null\n return createChatToolExecutor({\n provider,\n registry,\n collections: cms.collections,\n readers,\n mintDocumentId,\n candidates: candidateStore,\n onProposal: (toolCallId, proposal) => dispatch({ type: 'propose', toolCallId, proposal }),\n knowledge: () => knowledgeRef.current,\n // EC-378: the current-document seam, read at EXECUTION time — the\n // deterministic fallback when the model omits a tool's target.\n currentDocument: () => getAiCurrentDocument(),\n locale: activeLocale,\n ...(cms.localeConfig ? { localeConfig: cms.localeConfig } : {}),\n // EC-473 established that the assistant must validate with the EDITOR's\n // rules, not a subset. EC-497 hands over THE bag itself — one value, built\n // once by `gateValidateOptions` — so the assistant's gate and the editor's\n // gate cannot drift by someone forgetting a key at a call site.\n validateOptions: cms.validateOptions,\n ...(maxRepairAttempts !== undefined ? { maxRepairAttempts } : {}),\n })\n }, [\n provider,\n registry,\n cms.collections,\n cms.localeConfig,\n cms.validateOptions,\n readers,\n mintDocumentId,\n activeLocale,\n maxRepairAttempts,\n candidateStore,\n ])\n\n const chat = useChat({\n // A new conversation is a new chat id — `useChat` recreates its state.\n id: `elytra-assistant-${conversation}`,\n transport,\n onToolCall: async ({ toolCall }) => {\n if (!executor) return\n const outcome = await executor.execute({\n toolCallId: toolCall.toolCallId,\n toolName: toolCall.toolName,\n input: toolCall.input,\n })\n // Deliberately NOT awaited: `onToolCall` runs inside the chat's serial\n // stream job, and `addToolOutput` enqueues on the same executor —\n // awaiting it here deadlocks the stream (verified against ai@7.0.22).\n if (outcome.ok) {\n void chat.addToolOutput({\n tool: toolCall.toolName,\n toolCallId: toolCall.toolCallId,\n output: outcome.output,\n })\n } else {\n void chat.addToolOutput({\n state: 'output-error',\n tool: toolCall.toolName,\n toolCallId: toolCall.toolCallId,\n errorText: outcome.errorText,\n })\n }\n },\n // The SDK predicate wrapped with the per-user-turn step cap: a capped turn\n // stops cleanly with a visible note — never a silent stall (EC-334).\n sendAutomaticallyWhen: withStepCap(lastAssistantMessageIsCompleteWithToolCalls, {\n maxSteps: DEFAULT_MAX_STEPS,\n onCap: () =>\n dispatch({ type: 'fail', message: translate('ai.error.stepCap', { max: DEFAULT_MAX_STEPS }) }),\n }),\n // EC-514: \"no route mounted at <path>\" is named as itself; every other\n // transport/provider failure keeps the generic route note.\n onError: (error) =>\n dispatch({\n type: 'fail',\n message: isAiRouteNotMountedError(error)\n ? translate('ai.error.routeNotMounted', { endpoint: error.endpoint })\n : translate('ai.error.route'),\n }),\n })\n\n const busy = chat.status === 'submitted' || chat.status === 'streaming'\n\n const submit = useCallback(() => {\n const text = input.trim()\n if (text.length === 0 || busy || !ready) return\n setInput('')\n dispatch({ type: 'clear-error' })\n void chat.sendMessage({ text })\n }, [input, busy, ready, chat])\n\n // EC-380: clear / new topic — busy-safe (stop first), pending proposals\n // discarded with reject semantics (the review state resets; nothing was applied).\n const startNewConversation = useCallback(() => {\n void clearConversation({ status: chat.status, stop: chat.stop }, () => {\n dispatch({ type: 'clear' })\n setInput('')\n setConversation((current) => current + 1)\n })\n }, [chat.status, chat.stop])\n\n const applyProposal = useCallback(\n async (proposal: ProposalReview) => {\n if (projectId === undefined) return\n // For an update/translate of an EXISTING document, spread the stored doc\n // first so document-level extras (the alias) survive the apply —\n // the patch replaces only values/localized.\n const existing = cms.getDocument({ collection: proposal.collection, id: proposal.documentId })\n const doc: CmsDocument = {\n ...(existing ?? {}),\n collection: proposal.collection,\n id: proposal.documentId,\n values: proposal.patch.values,\n ...(proposal.patch.localized ? { localized: proposal.patch.localized } : {}),\n }\n // EC-473 — re-validate BEFORE the write. The proposal was validated when it was\n // built, but the editor may have changed the document since (or applied an\n // earlier proposal on top of it), so the patch is re-judged against what is in\n // the session NOW. Same gate as every other AI write path: only errors the\n // apply would INTRODUCE block it — pre-existing brokenness on the document is\n // not the assistant's doing and must not make an unrelated change unappliable.\n const introduced = introducedErrors(\n existing ? cms.validate(existing) : [],\n cms.validate(doc),\n )\n if (introduced.length > 0) {\n dispatch({\n type: 'fail',\n message: translate('ai.error.applyInvalid', {\n issues: introduced\n .slice(0, 3)\n .map((issue) => issue.message)\n .join('; '),\n }),\n })\n return\n }\n // EC-595 A2: an update / translation persists the POSITIONS it changes (the\n // proposal's patches) — never the merged whole document, which would revert any\n // change made since the proposal was built; a create is insert-if-absent. The\n // FORCED FLUSH first, so the open editor's uncommitted fields land before.\n const current = getAiCurrentDocument()\n const ref = { collection: proposal.collection, id: proposal.documentId }\n if (current && current.collection === ref.collection && current.id === ref.id) await current.flush()\n if (existing && proposal.patches) {\n // EC-647: AT the revision of the read the proposal was built from — a position\n // changed since then by anyone else is refused (the apply fails visibly), never\n // overwritten by content the assistant never saw.\n const outcome = await cms.commitPatches(\n ref,\n proposal.patches,\n proposal.patches.map(() => proposal.revision),\n )\n if (outcome?.status === 'failed' || outcome?.status === 'refused') {\n dispatch({ type: 'fail', message: translate('ai.error.applyFailed') })\n return\n }\n } else if (!existing) {\n cms.insertDocument(doc)\n await cms.flushPersistence(ref)\n } else {\n // An update without positions cannot be applied without overwriting — say so.\n dispatch({ type: 'fail', message: translate('ai.error.applyFailed') })\n return\n }\n const { operations } = await getStudioOperations()\n // Provenance: record an AI-tagged version snapshot (a restore point + the\n // 'assistant' origin surfaced in the versions pane). Best-effort — the draft\n // already landed above; a snapshot failure must not lose the applied content.\n try {\n await operations.documents.snapshot({\n projectId,\n ref: { collection: proposal.collection, id: proposal.documentId },\n label: translate('cms.version.aiAuthoredTitle'),\n origin: 'assistant',\n })\n } catch {\n // ignore — provenance is additive\n }\n // The session already shows the applied positions (the persister's view), and an\n // open editor rebases on it — its own uncommitted fields win (EC-595 point 7).\n dispatch({ type: 'apply', toolCallId: proposal.id })\n },\n [projectId, cms],\n )\n\n const openProposal = useCallback(\n (proposal: ProposalReview) => {\n if (projectId === undefined) return\n void navigate({\n to: STUDIO_PROJECT_ROUTE,\n params: { projectId, _splat: studioPath.cmsDocument(proposal.collection, proposal.documentId) },\n })\n },\n [navigate, projectId],\n )\n\n const hasProposal = useCallback(\n (toolCallId: string) => review.proposals[toolCallId] !== undefined,\n [review.proposals],\n )\n\n const renderAssistantMessage = (message: UIMessage) => {\n const segments = segmentMessageParts(message.parts as MessagePartView[], hasProposal)\n return segments.map((segment, index) => {\n if (segment.kind === 'text') {\n return (\n <AssistantTurn key={`${message.id}-${index}`}>\n <Text size=\"sm\" className=\"whitespace-pre-wrap break-words\">\n {segment.text}\n </Text>\n </AssistantTurn>\n )\n }\n if (segment.kind === 'tools') {\n return (\n <AssistantTurn key={`${message.id}-${index}`}>\n <ToolStatus steps={segment.parts.map(describeToolPart)} />\n </AssistantTurn>\n )\n }\n const proposal = review.proposals[segment.toolCallId]\n if (!proposal) return null\n return (\n <ProposalCard\n key={segment.toolCallId}\n proposal={proposal}\n onApply={() => void applyProposal(proposal)}\n onDiscard={() => dispatch({ type: 'reject', toolCallId: proposal.id })}\n onOpen={() => openProposal(proposal)}\n />\n )\n })\n }\n\n return (\n <aside className=\"flex h-full min-h-0 w-[360px] shrink-0 flex-col border-l border-border bg-card\">\n <Row gap=\"2\" align=\"center\" className=\"h-12 shrink-0 border-b border-border px-3\">\n <Heading level={2} size=\"h4\">\n {translate('ai.panel.title')}\n </Heading>\n <Spacer />\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n onClick={startNewConversation}\n disabled={chat.messages.length === 0 && review.error === null && !busy}\n title={translate('ai.panel.newConversation')}\n aria-label={translate('ai.panel.newConversation')}\n >\n <MessageSquarePlus />\n </Button>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n onClick={() => setAiPanelOpen(false)}\n title={translate('ai.panel.close')}\n aria-label={translate('ai.panel.close')}\n >\n <X />\n </Button>\n </Row>\n\n <Conversation atBottomKey={chat.messages}>\n {chat.messages.length === 0 ? <ConversationEmpty /> : null}\n {chat.messages.map((message) =>\n message.role === 'user' ? (\n <UserTurn key={message.id} text={messageText(message)} />\n ) : (\n <div key={message.id} className=\"contents\">\n {renderAssistantMessage(message)}\n </div>\n ),\n )}\n {chat.status === 'submitted' ? (\n <AssistantTurn>\n <WorkingIndicator />\n </AssistantTurn>\n ) : null}\n {review.error !== null ? (\n <AssistantTurn>\n <ChatErrorNote message={review.error} />\n </AssistantTurn>\n ) : null}\n </Conversation>\n\n {ready ? (\n <PromptInput value={input} onChange={setInput} onSubmit={submit} disabled={busy} />\n ) : (\n <div className=\"border-t border-border p-3\">\n <Text size=\"xs\" tone=\"muted\">\n {translate('ai.panel.unavailable')}\n </Text>\n </div>\n )}\n </aside>\n )\n}\n"]}
|
|
@@ -5,6 +5,12 @@ export interface TranslateDocumentActionProps {
|
|
|
5
5
|
docRef: DocumentRef;
|
|
6
6
|
/** The live editor draft — the translation source + the target for the fills. */
|
|
7
7
|
document: CmsDocument;
|
|
8
|
+
/**
|
|
9
|
+
* EC-647 — the revision the draft is at (`DocumentDraft.revision`): the translation is
|
|
10
|
+
* authored on {@link document}, so its fills are written AT this revision — a bucket a
|
|
11
|
+
* colleague changed since is refused on that field, never overwritten.
|
|
12
|
+
*/
|
|
13
|
+
revision: number | undefined;
|
|
8
14
|
localeConfig: LocaleConfig;
|
|
9
15
|
/** Whether the collection view exposes per-locale editing (`source.capabilities.locales`). */
|
|
10
16
|
localesEnabled: boolean;
|