@atlaskit/editor-plugin-show-diff 10.1.23 → 10.2.0
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/CHANGELOG.md +37 -0
- package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +132 -31
- package/dist/cjs/pm-plugins/calculateDiff/groupChangesByBlock.js +4 -2
- package/dist/cjs/pm-plugins/calculateDiff/smart/classifySmartChanges.js +841 -0
- package/dist/cjs/pm-plugins/calculateDiff/smart/helpers.js +135 -0
- package/dist/cjs/pm-plugins/calculateDiff/smart/segmentText.js +331 -0
- package/dist/cjs/pm-plugins/calculateDiff/smart/thresholds.js +46 -0
- package/dist/cjs/pm-plugins/decorations/createBlockChangedDecoration.js +24 -18
- package/dist/cjs/pm-plugins/decorations/createChangedRowDecorationWidgets.js +18 -12
- package/dist/cjs/pm-plugins/decorations/createInlineChangedDecoration.js +6 -5
- package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +30 -17
- package/dist/cjs/pm-plugins/decorations/decorationKeys.js +8 -5
- package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeView.js +52 -30
- package/dist/cjs/pm-plugins/getScrollableDecorations.js +6 -6
- package/dist/cjs/pm-plugins/isExtendedEnabled.js +20 -0
- package/dist/cjs/pm-plugins/main.js +21 -16
- package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +119 -29
- package/dist/es2019/pm-plugins/calculateDiff/groupChangesByBlock.js +4 -3
- package/dist/es2019/pm-plugins/calculateDiff/smart/classifySmartChanges.js +689 -0
- package/dist/es2019/pm-plugins/calculateDiff/smart/helpers.js +106 -0
- package/dist/es2019/pm-plugins/calculateDiff/smart/segmentText.js +283 -0
- package/dist/es2019/pm-plugins/calculateDiff/smart/thresholds.js +45 -0
- package/dist/es2019/pm-plugins/decorations/createBlockChangedDecoration.js +24 -18
- package/dist/es2019/pm-plugins/decorations/createChangedRowDecorationWidgets.js +18 -12
- package/dist/es2019/pm-plugins/decorations/createInlineChangedDecoration.js +6 -5
- package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +31 -16
- package/dist/es2019/pm-plugins/decorations/decorationKeys.js +9 -5
- package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeView.js +52 -34
- package/dist/es2019/pm-plugins/getScrollableDecorations.js +6 -6
- package/dist/es2019/pm-plugins/isExtendedEnabled.js +12 -0
- package/dist/es2019/pm-plugins/main.js +21 -16
- package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +132 -31
- package/dist/esm/pm-plugins/calculateDiff/groupChangesByBlock.js +4 -3
- package/dist/esm/pm-plugins/calculateDiff/smart/classifySmartChanges.js +834 -0
- package/dist/esm/pm-plugins/calculateDiff/smart/helpers.js +128 -0
- package/dist/esm/pm-plugins/calculateDiff/smart/segmentText.js +325 -0
- package/dist/esm/pm-plugins/calculateDiff/smart/thresholds.js +39 -0
- package/dist/esm/pm-plugins/decorations/createBlockChangedDecoration.js +24 -18
- package/dist/esm/pm-plugins/decorations/createChangedRowDecorationWidgets.js +18 -12
- package/dist/esm/pm-plugins/decorations/createInlineChangedDecoration.js +6 -5
- package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +30 -17
- package/dist/esm/pm-plugins/decorations/decorationKeys.js +9 -5
- package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeView.js +52 -30
- package/dist/esm/pm-plugins/getScrollableDecorations.js +6 -6
- package/dist/esm/pm-plugins/isExtendedEnabled.js +14 -0
- package/dist/esm/pm-plugins/main.js +21 -16
- package/dist/types/entry-points/show-diff-plugin-type.d.ts +1 -1
- package/dist/types/pm-plugins/calculateDiff/calculateDiffDecorations.d.ts +3 -1
- package/dist/types/pm-plugins/calculateDiff/groupChangesByBlock.d.ts +9 -0
- package/dist/types/pm-plugins/calculateDiff/smart/classifySmartChanges.d.ts +26 -0
- package/dist/types/pm-plugins/calculateDiff/smart/helpers.d.ts +44 -0
- package/dist/types/pm-plugins/calculateDiff/smart/segmentText.d.ts +32 -0
- package/dist/types/pm-plugins/calculateDiff/smart/thresholds.d.ts +44 -0
- package/dist/types/pm-plugins/decorations/createBlockChangedDecoration.d.ts +3 -2
- package/dist/types/pm-plugins/decorations/createChangedRowDecorationWidgets.d.ts +3 -2
- package/dist/types/pm-plugins/decorations/createInlineChangedDecoration.d.ts +3 -2
- package/dist/types/pm-plugins/decorations/createNodeChangedDecorationWidget.d.ts +4 -2
- package/dist/types/pm-plugins/decorations/decorationKeys.d.ts +5 -3
- package/dist/types/pm-plugins/decorations/utils/wrapBlockNodeView.d.ts +6 -4
- package/dist/types/pm-plugins/getScrollableDecorations.d.ts +2 -1
- package/dist/types/pm-plugins/isExtendedEnabled.d.ts +11 -0
- package/dist/types/pm-plugins/main.d.ts +11 -1
- package/dist/types/showDiffPluginType.d.ts +20 -1
- package/docs/smart-diff-design.md +240 -0
- package/package.json +7 -2
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# `smart` diffType — design
|
|
2
|
+
|
|
3
|
+
> Status: implemented behind the `platform_editor_ai_smart_diff` feature gate (which itself
|
|
4
|
+
> requires the extended diff pipeline; see §8). This document describes the **final**
|
|
5
|
+
> block-first implementation, not the original spike.
|
|
6
|
+
|
|
7
|
+
## 1. Problem
|
|
8
|
+
|
|
9
|
+
`editor-plugin-show-diff` renders a visual diff from an original document plus the ProseMirror
|
|
10
|
+
`steps` that produced the new document. It already supports three `diffType`s, each with a
|
|
11
|
+
failure mode:
|
|
12
|
+
|
|
13
|
+
- **`inline`** — word/character precise. Too granular: a lightly-reworded paragraph becomes a
|
|
14
|
+
confetti of tiny highlights that is hard to read.
|
|
15
|
+
- **`block`** — whole top-level block. Too coarse: a one-word fix highlights the entire
|
|
16
|
+
paragraph.
|
|
17
|
+
- **`step`** — per ProseMirror step. Inherits both problems depending on step shape; a single
|
|
18
|
+
`ReplaceStep` whose slice spans multiple nodes is especially bad.
|
|
19
|
+
|
|
20
|
+
We want an **intelligent** diff that adapts granularity to *how much* changed at the sentence,
|
|
21
|
+
paragraph and node level.
|
|
22
|
+
|
|
23
|
+
## 2. Goal
|
|
24
|
+
|
|
25
|
+
Introduce a new `diffType: 'smart'` that, for each changed region, chooses the smallest
|
|
26
|
+
readable granularity:
|
|
27
|
+
|
|
28
|
+
- A couple of changed words in a sentence → **inline** word diff.
|
|
29
|
+
- Many changed words in a sentence → **whole-sentence** diff.
|
|
30
|
+
- A couple of changed sentences in a paragraph → **sentence** diffs inside the paragraph.
|
|
31
|
+
- Many changed sentences in a paragraph → **whole-paragraph** diff.
|
|
32
|
+
- Many changed children in a container (layout, table, list, panel, quote, …) → **node-level**
|
|
33
|
+
diff (old node shown deleted, new node shown inserted).
|
|
34
|
+
- A node whose *type/wrapping* changed (paragraph→heading, list→table, wrap in panel/quote/…) →
|
|
35
|
+
**node-level** diff.
|
|
36
|
+
|
|
37
|
+
Crucially, granularity must never **break document structure** (see §5).
|
|
38
|
+
|
|
39
|
+
## 3. Where it plugs in
|
|
40
|
+
|
|
41
|
+
The classifier is a pure `Change[] → Change[]` transform. It runs inside `getChanges` in
|
|
42
|
+
`calculateDiff/calculateDiffDecorations.ts`:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
if (isExtendedEnabled(diffType)) {
|
|
46
|
+
if (diffType === 'smart' && fg('platform_editor_ai_smart_diff')) {
|
|
47
|
+
const changes = simplifyChanges(changeset.changes, tr.doc);
|
|
48
|
+
return classifySmartChanges({ changes, originalDoc, newDoc: tr.doc,
|
|
49
|
+
locale: intl.locale, thresholds: smartThresholds });
|
|
50
|
+
}
|
|
51
|
+
if (diffType === 'step') return diffBySteps(originalDoc, steps);
|
|
52
|
+
if (diffType === 'block') return groupChangesByBlock(changeset.changes, originalDoc, steppedDoc);
|
|
53
|
+
}
|
|
54
|
+
// otherwise inline:
|
|
55
|
+
return optimizeChanges(simplifyChanges(changeset.changes, tr.doc));
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Key properties:
|
|
59
|
+
|
|
60
|
+
- **Input** is the same prosemirror-changeset `Change[]` (`simplifyChanges`) that `inline`
|
|
61
|
+
consumes. We do **not** re-diff — we re-*group* and re-*size* the existing changes.
|
|
62
|
+
- **Output** is a `Change[]` in exactly the same shape, so the entire downstream decoration
|
|
63
|
+
layer (inline highlights, block backgrounds, node/row deleted widgets, indicators) is reused
|
|
64
|
+
unchanged.
|
|
65
|
+
- If `platform_editor_ai_smart_diff` is off, `smart` **falls through to `inline`** so behaviour
|
|
66
|
+
degrades gracefully.
|
|
67
|
+
|
|
68
|
+
Each promoted change carries a `smartLevel` tag (`'sentence' | 'paragraph' | 'node'`) on its
|
|
69
|
+
span data, read downstream by `smartChangeLevel(change)` to drive node-level rendering choices
|
|
70
|
+
(skip inline highlight, place deleted content below the new content).
|
|
71
|
+
|
|
72
|
+
## 4. Algorithm — block-first (the key design decision)
|
|
73
|
+
|
|
74
|
+
The original spike was *change-first* (grouped by each raw change and walked outward). It
|
|
75
|
+
produced subtle position bugs — most visibly the "empty deleted list items" when a `bulletList`
|
|
76
|
+
became a `table` — because its ad-hoc block resolution did not match how the `block` diffType
|
|
77
|
+
groups. `block` handled these cases perfectly.
|
|
78
|
+
|
|
79
|
+
The final design is therefore **block-first, reusing the exact same grouping primitive as the
|
|
80
|
+
`block` diffType** (`getTopLevelBlockAt`). This makes smart's top-level grouping identical to
|
|
81
|
+
block's *by construction*.
|
|
82
|
+
|
|
83
|
+
Pipeline (`classifySmartChanges`):
|
|
84
|
+
|
|
85
|
+
1. **Group by top-level block** (`groupByTopLevelBlock`) using `getTopLevelBlockAt(docB, …)` and
|
|
86
|
+
`getTopLevelBlockAt(docA, …)`, keyed by the new-doc block start. Unlike `groupChangesByBlock`,
|
|
87
|
+
we keep each group's constituent raw changes so intra-block density can be measured.
|
|
88
|
+
2. **Classify each block group** (`classifyBlockGroup`):
|
|
89
|
+
- **Missing side** (block only in A or only in B) → whole-block change.
|
|
90
|
+
- **Type / wrapping change** (A block type ≠ B block type) → whole-block change. This is the
|
|
91
|
+
paragraph→heading, list→table, wrap-in-panel/quote case.
|
|
92
|
+
- **Textblock** (paragraph/heading on both sides) → `classifyTextblock` (§4.1).
|
|
93
|
+
- **Container** (layout, table, list, panel, expand, quote, …) → `classifyContainer` (§4.2).
|
|
94
|
+
3. **Merge overlaps** (`mergeOverlappingByNewDocRange`) and clamp to doc bounds.
|
|
95
|
+
|
|
96
|
+
### 4.1 Textblock — sentence vs paragraph vs inline
|
|
97
|
+
|
|
98
|
+
`classifyTextblock` segments both sides with `Intl.Segmenter` (graceful regex fallback — see §6),
|
|
99
|
+
treating inline non-text nodes as one opaque word that is **never** a sentence boundary.
|
|
100
|
+
|
|
101
|
+
- Compute `sentencesChanged / max(sentencesOld, sentencesNew)`.
|
|
102
|
+
- If `sentencesChanged >= paragraph.minChanged` **and** ratio `>= paragraph.ratio` →
|
|
103
|
+
**whole-paragraph** change (`smartLevel: 'paragraph'`).
|
|
104
|
+
- Otherwise, per changed sentence, compute `wordsChanged / max(wordsOld, wordsNew)`:
|
|
105
|
+
- `wordsChanged >= sentence.minChanged` **and** ratio `>= sentence.ratio` →
|
|
106
|
+
**whole-sentence** change (`smartLevel: 'sentence'`).
|
|
107
|
+
- else keep the raw **inline** change(s).
|
|
108
|
+
|
|
109
|
+
### 4.2 Container — recurse with escalation
|
|
110
|
+
|
|
111
|
+
`classifyContainer` measures density over **direct children**:
|
|
112
|
+
`changedChildren / max(childrenOld, childrenNew)`.
|
|
113
|
+
|
|
114
|
+
- If `>= node.ratio` (default 0.6) → **whole-node** change.
|
|
115
|
+
- Else, an alternative text-bearing trigger: if the fraction of text-bearing children
|
|
116
|
+
(paragraph/heading) that were themselves promoted to paragraph-level is `>= node.textBearingRatio`
|
|
117
|
+
→ **whole-node** change.
|
|
118
|
+
- Otherwise, recurse into each changed child (`classifyChild`), keeping the diff strictly inside.
|
|
119
|
+
|
|
120
|
+
Tables are special-cased (`classifyTable`): cell density is escalated to the **row**, and row
|
|
121
|
+
density to the **table** (see §5).
|
|
122
|
+
|
|
123
|
+
## 5. Structural integrity — the escalation rule
|
|
124
|
+
|
|
125
|
+
Some children are **structurally rigid**: deleting and re-inserting one on its own would break
|
|
126
|
+
the parent's structure. These are tracked in `RIGID_CHILD_TYPES`:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
layoutColumn, tableCell, tableHeader
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Rules (`classifyChild` / `classifyContainer` / `classifyTable`):
|
|
133
|
+
|
|
134
|
+
- **`layoutColumn`** — never a whole-block result on its own. A dense column does **not** become
|
|
135
|
+
"column deleted + column added" (that would break the `layoutSection`). Instead we either
|
|
136
|
+
promote the **whole `layoutSection`** (when overall section density ≥ `node.ratio`) or keep the
|
|
137
|
+
diff **inside** the column (recurse into its children).
|
|
138
|
+
- **`tableCell` / `tableHeader`** — same: a single dense cell never becomes a deleted+added cell.
|
|
139
|
+
Cell density escalates to the **`tableRow`** (a row *can* be shown deleted + re-inserted without
|
|
140
|
+
breaking the table), and row density escalates to the **whole `table`**. So even if one cell is
|
|
141
|
+
100% changed, if its row is < `node.ratio` changed overall, the diff stays inline inside the
|
|
142
|
+
cell.
|
|
143
|
+
- **`listItem`** — *not* rigid. A list item *can* be replaced wholesale (delete + re-insert at the
|
|
144
|
+
same position keeps the list valid), so it follows the normal container rule: promote the whole
|
|
145
|
+
item when dense, else recurse. And when ≥ `node.ratio` of items changed, the **whole list** is
|
|
146
|
+
promoted.
|
|
147
|
+
|
|
148
|
+
This is why the doc emphasises escalation over naive "promote the nearest container".
|
|
149
|
+
|
|
150
|
+
## 6. Configurable thresholds
|
|
151
|
+
|
|
152
|
+
`SmartDiffThresholds` (in `smart/thresholds.ts`) is passed from the `showDiff` command via
|
|
153
|
+
`smartThresholds` → plugin state → `calculateDiffDecorations` → `classifySmartChanges`.
|
|
154
|
+
Overrides are shallow-merged per level (`resolveThresholds`).
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
DEFAULT_SMART_THRESHOLDS = {
|
|
158
|
+
sentence: { ratio: 0.4, minChanged: 2 }, // unit = words (inline node = 1 word)
|
|
159
|
+
paragraph: { ratio: 0.4, minChanged: 2 }, // unit = sentences
|
|
160
|
+
node: { ratio: 0.6, textBearingRatio: 0.6 }, // unit = direct children
|
|
161
|
+
};
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- **sentence** — numerator = words changed; denominator = `max(wordsOld, wordsNew)`;
|
|
165
|
+
`minChanged` = 2.
|
|
166
|
+
- **paragraph** — numerator = sentences changed; denominator = `max(sentencesOld, sentencesNew)`;
|
|
167
|
+
`minChanged` = 2.
|
|
168
|
+
- **node** — numerator = direct children changed; denominator = `max(childrenOld, childrenNew)`;
|
|
169
|
+
ratio = 0.6. The `textBearingRatio` alternative fires node-level when ≥ 60% of the text-bearing
|
|
170
|
+
children were themselves promoted to paragraph-level.
|
|
171
|
+
|
|
172
|
+
### Segmentation (`smart/segmentText.ts`)
|
|
173
|
+
|
|
174
|
+
- Primary: `Intl.Segmenter` for both sentence and word granularity (locale from `intl.locale`).
|
|
175
|
+
- **Graceful regex fallback** when `Intl.Segmenter` is unavailable: sentences split on terminator
|
|
176
|
+
punctuation, words on whitespace/word boundaries.
|
|
177
|
+
- Operates on a `(string | null)[]` char-view where `null` marks an **opaque inline token**
|
|
178
|
+
(date, mention, emoji, status, …). An inline token counts as exactly one word and is **never**
|
|
179
|
+
a sentence boundary.
|
|
180
|
+
|
|
181
|
+
## 7. Type/wrapping changes & attribute annotation
|
|
182
|
+
|
|
183
|
+
- A pure **attribute** change (same node type, same text) is annotated by the existing
|
|
184
|
+
`AttrStep`/`SetAttrsStep` path — not by smart.
|
|
185
|
+
- A **node-type or wrapping** change (paragraph→heading, list→table, wrap in panel/quote/
|
|
186
|
+
codeBlock) cannot be an attribute annotation, because the node types differ. Smart detects this
|
|
187
|
+
at the block-group level (A type ≠ B type) and emits a **whole-block** (node-level) change: old
|
|
188
|
+
node shown deleted, new node shown inserted.
|
|
189
|
+
|
|
190
|
+
## 8. Feature gating
|
|
191
|
+
|
|
192
|
+
`smart` requires **both** the extended diff pipeline behaviour **and** its own gate. The single
|
|
193
|
+
source of truth is `pm-plugins/isExtendedEnabled.ts`:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
export const isExtendedEnabled = (diffType?: DiffType): boolean =>
|
|
197
|
+
expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true) ||
|
|
198
|
+
(diffType === 'smart' && fg('platform_editor_ai_smart_diff'));
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Because the whole extended decoration pipeline was previously gated only by
|
|
202
|
+
`platform_editor_diff_plugin_extended`, `diffType` is **threaded through the entire decoration
|
|
203
|
+
layer** (inline/block/node/row decorations, `wrapBlockNodeView` and its helpers,
|
|
204
|
+
`decorationKeys`, `getScrollableDecorations`) so that every internal extended-gate check becomes
|
|
205
|
+
`isExtendedEnabled(diffType)`. This makes `smart` render the full extended shape even when
|
|
206
|
+
`platform_editor_diff_plugin_extended` is off — and only when `platform_editor_ai_smart_diff` is
|
|
207
|
+
on. Other diff types are unaffected (they pass `diffType` that is not `'smart'`, so the check
|
|
208
|
+
reduces to the extended gate alone).
|
|
209
|
+
|
|
210
|
+
## 9. Rendering specifics
|
|
211
|
+
|
|
212
|
+
- **Node- and paragraph-level** smart changes render the **new (purple) content on top** and the
|
|
213
|
+
**deleted (gray, strikethrough) content below** it (`placeBelow`), so the new content always
|
|
214
|
+
reads first.
|
|
215
|
+
- For **node-level** smart changes the redundant inline highlight is skipped (`isSmartNodeLevel`),
|
|
216
|
+
since the block/widget decorations already highlight the container.
|
|
217
|
+
|
|
218
|
+
## 10. Module layout
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
src/pm-plugins/
|
|
222
|
+
isExtendedEnabled.ts ← shared gate helper (diffType-aware)
|
|
223
|
+
calculateDiff/smart/
|
|
224
|
+
classifySmartChanges.ts ← block-first orchestrator (§4, §5)
|
|
225
|
+
thresholds.ts ← SmartDiffThresholds + defaults (§6)
|
|
226
|
+
segmentText.ts ← Intl.Segmenter + regex fallback (§6)
|
|
227
|
+
helpers.ts ← makePromotedChange, merge/overlap, smartLevel tag
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## 11. Testing
|
|
231
|
+
|
|
232
|
+
- `segmentText` unit tests live in
|
|
233
|
+
`editor-plugin-show-diff-tests/src/__tests__/jest/segmentText.test.ts` and cover the
|
|
234
|
+
Intl.Segmenter path and the regex fallback (inline token = one word, never a sentence
|
|
235
|
+
boundary).
|
|
236
|
+
- Classifier behaviour is exercised end-to-end via the `examples/3-smart.tsx` gallery (scenarios
|
|
237
|
+
1a–C6) covering inline/sentence/paragraph promotion, list→table, table cells, layout columns,
|
|
238
|
+
panels, quotes, code blocks, and multi-step changes.
|
|
239
|
+
|
|
240
|
+
Run: `afm test unit packages/editor/editor-plugin-show-diff-tests/src/__tests__/jest/segmentText.test.ts --run-in-band`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/editor-plugin-show-diff",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.2.0",
|
|
4
4
|
"description": "ShowDiff plugin for @atlaskit/editor-core",
|
|
5
5
|
"author": "Atlassian Pty Ltd",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"@atlaskit/adf-utils": "^20.3.0",
|
|
41
41
|
"@atlaskit/button": "^24.3.0",
|
|
42
42
|
"@atlaskit/css": "^1.0.0",
|
|
43
|
+
"@atlaskit/dropdown-menu": "^17.1.0",
|
|
43
44
|
"@atlaskit/editor-core": "^221.7.0",
|
|
44
45
|
"@atlaskit/editor-json-transformer": "^9.0.0",
|
|
45
46
|
"@atlaskit/form": "^16.1.0",
|
|
@@ -92,5 +93,9 @@
|
|
|
92
93
|
]
|
|
93
94
|
}
|
|
94
95
|
},
|
|
95
|
-
"platform-feature-flags": {
|
|
96
|
+
"platform-feature-flags": {
|
|
97
|
+
"platform_editor_ai_smart_diff": {
|
|
98
|
+
"type": "boolean"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
96
101
|
}
|