@malva-ui/editor 0.1.13 → 0.1.14
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/README.md +5 -5
- package/fesm2022/malva-ui-editor-ai.mjs +17 -0
- package/fesm2022/malva-ui-editor-ai.mjs.map +1 -0
- package/fesm2022/malva-ui-editor.mjs +10227 -0
- package/fesm2022/malva-ui-editor.mjs.map +1 -0
- package/package.json +35 -26
- package/types/malva-ui-editor-ai.d.ts +1 -0
- package/types/malva-ui-editor.d.ts +2995 -0
- package/ai/index.d.ts +0 -12
- package/ai/malva-ui-editor-ai.d.ts +0 -5
- package/esm2022/ai/index.js +0 -12
- package/esm2022/ai/index.js.map +0 -1
- package/esm2022/ai/malva-ui-editor-ai.js +0 -5
- package/esm2022/ai/malva-ui-editor-ai.js.map +0 -1
- package/esm2022/index.js +0 -37
- package/esm2022/index.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-actions.js +0 -69
- package/esm2022/lib/ai/editor-ai-actions.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-context.js +0 -612
- package/esm2022/lib/ai/editor-ai-context.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-menu.js +0 -288
- package/esm2022/lib/ai/editor-ai-menu.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-review-bar.js +0 -223
- package/esm2022/lib/ai/editor-ai-review-bar.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-stream.js +0 -677
- package/esm2022/lib/ai/editor-ai-stream.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-suggestions.js +0 -751
- package/esm2022/lib/ai/editor-ai-suggestions.js.map +0 -1
- package/esm2022/lib/ai/editor-ai.tokens.js +0 -10
- package/esm2022/lib/ai/editor-ai.tokens.js.map +0 -1
- package/esm2022/lib/ai/editor-ai.types.js +0 -1
- package/esm2022/lib/ai/editor-ai.types.js.map +0 -1
- package/esm2022/lib/editor/editor.js +0 -985
- package/esm2022/lib/editor/editor.js.map +0 -1
- package/esm2022/lib/editor-extension-preflight.js +0 -83
- package/esm2022/lib/editor-extension-preflight.js.map +0 -1
- package/esm2022/lib/editor-serialization.js +0 -152
- package/esm2022/lib/editor-serialization.js.map +0 -1
- package/esm2022/lib/editor-toolbar-context.js +0 -259
- package/esm2022/lib/editor-toolbar-context.js.map +0 -1
- package/esm2022/lib/editor.tokens.js +0 -10
- package/esm2022/lib/editor.tokens.js.map +0 -1
- package/esm2022/lib/editor.types.js +0 -48
- package/esm2022/lib/editor.types.js.map +0 -1
- package/esm2022/lib/extensions/editor-block-handle.js +0 -760
- package/esm2022/lib/extensions/editor-block-handle.js.map +0 -1
- package/esm2022/lib/extensions/editor-extensions.js +0 -321
- package/esm2022/lib/extensions/editor-extensions.js.map +0 -1
- package/esm2022/lib/extensions/editor-markdown-compatibility.js +0 -162
- package/esm2022/lib/extensions/editor-markdown-compatibility.js.map +0 -1
- package/esm2022/lib/extensions/editor-upload-placeholder.js +0 -156
- package/esm2022/lib/extensions/editor-upload-placeholder.js.map +0 -1
- package/esm2022/lib/status/editor-status.js +0 -64
- package/esm2022/lib/status/editor-status.js.map +0 -1
- package/esm2022/lib/toolbar/editor-alignment.js +0 -203
- package/esm2022/lib/toolbar/editor-alignment.js.map +0 -1
- package/esm2022/lib/toolbar/editor-block-insert.js +0 -101
- package/esm2022/lib/toolbar/editor-block-insert.js.map +0 -1
- package/esm2022/lib/toolbar/editor-color-control.js +0 -71
- package/esm2022/lib/toolbar/editor-color-control.js.map +0 -1
- package/esm2022/lib/toolbar/editor-command-button.js +0 -115
- package/esm2022/lib/toolbar/editor-command-button.js.map +0 -1
- package/esm2022/lib/toolbar/editor-heading.js +0 -266
- package/esm2022/lib/toolbar/editor-heading.js.map +0 -1
- package/esm2022/lib/toolbar/editor-highlight.js +0 -84
- package/esm2022/lib/toolbar/editor-highlight.js.map +0 -1
- package/esm2022/lib/toolbar/editor-image-upload-dialog.js +0 -289
- package/esm2022/lib/toolbar/editor-image-upload-dialog.js.map +0 -1
- package/esm2022/lib/toolbar/editor-image-upload.js +0 -315
- package/esm2022/lib/toolbar/editor-image-upload.js.map +0 -1
- package/esm2022/lib/toolbar/editor-inline-marks.js +0 -124
- package/esm2022/lib/toolbar/editor-inline-marks.js.map +0 -1
- package/esm2022/lib/toolbar/editor-link.js +0 -265
- package/esm2022/lib/toolbar/editor-link.js.map +0 -1
- package/esm2022/lib/toolbar/editor-list.js +0 -249
- package/esm2022/lib/toolbar/editor-list.js.map +0 -1
- package/esm2022/lib/toolbar/editor-table.js +0 -292
- package/esm2022/lib/toolbar/editor-table.js.map +0 -1
- package/esm2022/lib/toolbar/editor-text-color.js +0 -82
- package/esm2022/lib/toolbar/editor-text-color.js.map +0 -1
- package/esm2022/lib/toolbar/editor-toolbar-widget.js +0 -41
- package/esm2022/lib/toolbar/editor-toolbar-widget.js.map +0 -1
- package/esm2022/lib/toolbar/editor-toolbar.defs.js +0 -60
- package/esm2022/lib/toolbar/editor-toolbar.defs.js.map +0 -1
- package/esm2022/lib/toolbar/editor-toolbar.js +0 -826
- package/esm2022/lib/toolbar/editor-toolbar.js.map +0 -1
- package/esm2022/lib/toolbar/editor-undo-redo.js +0 -72
- package/esm2022/lib/toolbar/editor-undo-redo.js.map +0 -1
- package/esm2022/lib/toolbar/editor-zoom.js +0 -330
- package/esm2022/lib/toolbar/editor-zoom.js.map +0 -1
- package/esm2022/lib/upload/editor-image-upload-coordinator.js +0 -480
- package/esm2022/lib/upload/editor-image-upload-coordinator.js.map +0 -1
- package/esm2022/malva-ui-editor.js +0 -5
- package/esm2022/malva-ui-editor.js.map +0 -1
- package/index.d.ts +0 -35
- package/lib/ai/editor-ai-actions.d.ts +0 -27
- package/lib/ai/editor-ai-context.d.ts +0 -307
- package/lib/ai/editor-ai-menu.d.ts +0 -142
- package/lib/ai/editor-ai-review-bar.d.ts +0 -116
- package/lib/ai/editor-ai-stream.d.ts +0 -207
- package/lib/ai/editor-ai-suggestions.d.ts +0 -219
- package/lib/ai/editor-ai.tokens.d.ts +0 -10
- package/lib/ai/editor-ai.types.d.ts +0 -176
- package/lib/editor/editor.d.ts +0 -243
- package/lib/editor-extension-preflight.d.ts +0 -23
- package/lib/editor-serialization.d.ts +0 -74
- package/lib/editor-toolbar-context.d.ts +0 -116
- package/lib/editor.tokens.d.ts +0 -6
- package/lib/editor.types.d.ts +0 -194
- package/lib/extensions/editor-block-handle.d.ts +0 -59
- package/lib/extensions/editor-extensions.d.ts +0 -165
- package/lib/extensions/editor-markdown-compatibility.d.ts +0 -11
- package/lib/extensions/editor-upload-placeholder.d.ts +0 -75
- package/lib/status/editor-status.d.ts +0 -24
- package/lib/toolbar/editor-alignment.d.ts +0 -61
- package/lib/toolbar/editor-block-insert.d.ts +0 -26
- package/lib/toolbar/editor-color-control.d.ts +0 -32
- package/lib/toolbar/editor-command-button.d.ts +0 -29
- package/lib/toolbar/editor-heading.d.ts +0 -68
- package/lib/toolbar/editor-highlight.d.ts +0 -21
- package/lib/toolbar/editor-image-upload-dialog.d.ts +0 -96
- package/lib/toolbar/editor-image-upload.d.ts +0 -71
- package/lib/toolbar/editor-inline-marks.d.ts +0 -30
- package/lib/toolbar/editor-link.d.ts +0 -78
- package/lib/toolbar/editor-list.d.ts +0 -67
- package/lib/toolbar/editor-table.d.ts +0 -133
- package/lib/toolbar/editor-text-color.d.ts +0 -21
- package/lib/toolbar/editor-toolbar-widget.d.ts +0 -19
- package/lib/toolbar/editor-toolbar.d.ts +0 -157
- package/lib/toolbar/editor-toolbar.defs.d.ts +0 -35
- package/lib/toolbar/editor-undo-redo.d.ts +0 -26
- package/lib/toolbar/editor-zoom.d.ts +0 -108
- package/lib/upload/editor-image-upload-coordinator.d.ts +0 -55
- package/malva-ui-editor.d.ts +0 -5
|
@@ -1,677 +0,0 @@
|
|
|
1
|
-
import { closeHistory } from '@tiptap/pm/history';
|
|
2
|
-
import { Node as ProseMirrorNode } from '@tiptap/pm/model';
|
|
3
|
-
import { Plugin, PluginKey, TextSelection } from '@tiptap/pm/state';
|
|
4
|
-
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
|
5
|
-
/**
|
|
6
|
-
* CSS class of the inline decoration marking the active streaming region.
|
|
7
|
-
*
|
|
8
|
-
* The class is applied through a ProseMirror decoration held in plugin state,
|
|
9
|
-
* never through document content, so it can appear in no HTML, Markdown, or
|
|
10
|
-
* JSON serialization.
|
|
11
|
-
*/
|
|
12
|
-
export const MLV_EDITOR_AI_STREAMING_CLASS = 'mlv-editor__ai-streaming';
|
|
13
|
-
/**
|
|
14
|
-
* CSS class of the one-shot inline decoration over each flush's newly
|
|
15
|
-
* revealed slice of the streaming region.
|
|
16
|
-
*
|
|
17
|
-
* Created exactly once per revealed delta and only mapped afterwards — never
|
|
18
|
-
* rebuilt for old ranges — so a one-shot CSS entrance animation runs once per
|
|
19
|
-
* chunk and earlier text never re-animates. Removed with the region
|
|
20
|
-
* decoration on commit, cancel, and abandonment, and excluded from every
|
|
21
|
-
* serialization the same way.
|
|
22
|
-
*/
|
|
23
|
-
export const MLV_EDITOR_AI_STREAMING_CHUNK_CLASS = 'mlv-editor__ai-streaming-chunk';
|
|
24
|
-
/**
|
|
25
|
-
* CSS class of the `aria-hidden` widget decoration the streaming engine
|
|
26
|
-
* keeps at the current insertion tip while a writing session is active.
|
|
27
|
-
*
|
|
28
|
-
* View-layer only: it exists in no HTML, Markdown, or JSON serialization and
|
|
29
|
-
* is removed when the session settles (commit, cancel, abandonment, failure).
|
|
30
|
-
* Collect-only `'review'` sessions never render it — they never write.
|
|
31
|
-
*/
|
|
32
|
-
export const MLV_EDITOR_AI_CARET_CLASS = 'mlv-editor__ai-caret';
|
|
33
|
-
/**
|
|
34
|
-
* @internal Key identifying the per-session streaming plugin. Every
|
|
35
|
-
* transaction the session dispatches carries metadata under this key, which
|
|
36
|
-
* is also how the external-transaction listener tells session writes apart
|
|
37
|
-
* from foreign document changes.
|
|
38
|
-
*/
|
|
39
|
-
const MLV_EDITOR_AI_STREAM_KEY = new PluginKey('mlvEditorAiStream');
|
|
40
|
-
/** @internal Metadata clearing the region on restore and commit transactions. */
|
|
41
|
-
const MLV_EDITOR_AI_CLEAR_METADATA = {
|
|
42
|
-
type: 'clear',
|
|
43
|
-
};
|
|
44
|
-
/** @internal Empty plugin state shared by init and every clear. */
|
|
45
|
-
const MLV_EDITOR_AI_EMPTY_PLUGIN_STATE = {
|
|
46
|
-
region: null,
|
|
47
|
-
decorations: DecorationSet.empty,
|
|
48
|
-
};
|
|
49
|
-
/**
|
|
50
|
-
* @internal Default {@link MlvEditorAiStreamOptions.revealCharsPerFrame}:
|
|
51
|
-
* about four characters per frame reads as fast, steady typing at 60 fps
|
|
52
|
-
* without the per-chunk lurch of uncapped writes.
|
|
53
|
-
*/
|
|
54
|
-
const MLV_EDITOR_AI_DEFAULT_REVEAL_CAP = 4;
|
|
55
|
-
/**
|
|
56
|
-
* @internal Spec marker identifying the caret widget inside the decoration
|
|
57
|
-
* set, so each flush can remove the previous caret before re-anchoring it at
|
|
58
|
-
* the new insertion tip.
|
|
59
|
-
*/
|
|
60
|
-
const MLV_EDITOR_AI_CARET_SPEC = 'mlvEditorAiCaret';
|
|
61
|
-
/**
|
|
62
|
-
* @internal Spec marker identifying the whole-region tint decoration, which
|
|
63
|
-
* is rebuilt on every flush because its range grows — unlike the per-chunk
|
|
64
|
-
* decorations, which are created once and only mapped.
|
|
65
|
-
*/
|
|
66
|
-
const MLV_EDITOR_AI_REGION_SPEC = 'mlvEditorAiRegion';
|
|
67
|
-
/**
|
|
68
|
-
* @internal Normalizes the reveal cap: `Infinity` is honoured as "uncapped",
|
|
69
|
-
* unusable values fall back to the default, fractions floor.
|
|
70
|
-
*/
|
|
71
|
-
const normalizeRevealCap = (value) => {
|
|
72
|
-
if (value === Number.POSITIVE_INFINITY)
|
|
73
|
-
return value;
|
|
74
|
-
if (value === undefined || !Number.isFinite(value) || value < 1) {
|
|
75
|
-
return MLV_EDITOR_AI_DEFAULT_REVEAL_CAP;
|
|
76
|
-
}
|
|
77
|
-
return Math.floor(value);
|
|
78
|
-
};
|
|
79
|
-
/**
|
|
80
|
-
* @internal Builds the caret widget decoration at the given insertion tip.
|
|
81
|
-
* The element is created lazily by the view (browser-only, so module scope
|
|
82
|
-
* stays SSR-safe) and is `aria-hidden`: the caret is a purely visual pulse,
|
|
83
|
-
* not content. `side: 1` keeps it after the text already revealed. The `key`
|
|
84
|
-
* lets ProseMirror treat consecutive carets at one position as identical.
|
|
85
|
-
*/
|
|
86
|
-
const createCaretDecoration = (tip) => Decoration.widget(tip, () => {
|
|
87
|
-
const caret = document.createElement('span');
|
|
88
|
-
caret.className = MLV_EDITOR_AI_CARET_CLASS;
|
|
89
|
-
caret.setAttribute('aria-hidden', 'true');
|
|
90
|
-
return caret;
|
|
91
|
-
}, {
|
|
92
|
-
key: MLV_EDITOR_AI_CARET_SPEC,
|
|
93
|
-
side: 1,
|
|
94
|
-
ignoreSelection: true,
|
|
95
|
-
[MLV_EDITOR_AI_CARET_SPEC]: true,
|
|
96
|
-
});
|
|
97
|
-
/**
|
|
98
|
-
* @internal One active session per editor. Starting a new session cancels the
|
|
99
|
-
* previous one first, so the shared plugin key can never be registered twice.
|
|
100
|
-
*/
|
|
101
|
-
const activeSessions = new WeakMap();
|
|
102
|
-
/**
|
|
103
|
-
* @internal Default frame scheduler: `requestAnimationFrame` where available,
|
|
104
|
-
* otherwise a short timeout so streaming still progresses under server
|
|
105
|
-
* rendering or a test DOM without a frame API. Feature-detected at call time —
|
|
106
|
-
* module scope stays SSR-safe.
|
|
107
|
-
*/
|
|
108
|
-
const defaultScheduler = (flush) => {
|
|
109
|
-
if (typeof requestAnimationFrame === 'function' &&
|
|
110
|
-
typeof cancelAnimationFrame === 'function') {
|
|
111
|
-
const handle = requestAnimationFrame(() => flush());
|
|
112
|
-
return () => cancelAnimationFrame(handle);
|
|
113
|
-
}
|
|
114
|
-
const handle = setTimeout(flush, 16);
|
|
115
|
-
return () => clearTimeout(handle);
|
|
116
|
-
};
|
|
117
|
-
/**
|
|
118
|
-
* @internal Creates the per-session ProseMirror plugin holding the streaming
|
|
119
|
-
* region and rendering it as an inline decoration. Decorations are view-layer
|
|
120
|
-
* state, so the region reaches no HTML, Markdown, or JSON serialization —
|
|
121
|
-
* the upload-placeholder precedent.
|
|
122
|
-
*
|
|
123
|
-
* The plugin also claims Escape while it is registered — which is exactly the
|
|
124
|
-
* session's lifetime — so a keyboard user whose focus is in the content
|
|
125
|
-
* region can cancel a running transform without reaching for the toolbar
|
|
126
|
-
* stop button.
|
|
127
|
-
*
|
|
128
|
-
* @param onEscape Invoked when Escape is pressed inside the content region.
|
|
129
|
-
*/
|
|
130
|
-
function createStreamPlugin(onEscape) {
|
|
131
|
-
return new Plugin({
|
|
132
|
-
key: MLV_EDITOR_AI_STREAM_KEY,
|
|
133
|
-
state: {
|
|
134
|
-
init: () => MLV_EDITOR_AI_EMPTY_PLUGIN_STATE,
|
|
135
|
-
apply: (transaction, value) => {
|
|
136
|
-
const metadata = transaction.getMeta(MLV_EDITOR_AI_STREAM_KEY);
|
|
137
|
-
if (metadata?.type === 'clear')
|
|
138
|
-
return MLV_EDITOR_AI_EMPTY_PLUGIN_STATE;
|
|
139
|
-
// Existing chunk decorations are only ever mapped — never rebuilt —
|
|
140
|
-
// so their DOM survives later flushes and the one-shot entrance
|
|
141
|
-
// animation cannot re-run over earlier text.
|
|
142
|
-
let decorations = transaction.docChanged
|
|
143
|
-
? value.decorations.map(transaction.mapping, transaction.doc)
|
|
144
|
-
: value.decorations;
|
|
145
|
-
if (metadata?.type === 'set') {
|
|
146
|
-
// The region tint (its range grew) and the caret (it moved to the
|
|
147
|
-
// new tip) are replaced; the newly revealed slice gets its own
|
|
148
|
-
// freshly created one-shot chunk decoration.
|
|
149
|
-
decorations = decorations.remove(decorations.find(undefined, undefined, (spec) => spec[MLV_EDITOR_AI_CARET_SPEC] === true ||
|
|
150
|
-
spec[MLV_EDITOR_AI_REGION_SPEC] === true));
|
|
151
|
-
const added = [];
|
|
152
|
-
if (metadata.from < metadata.to) {
|
|
153
|
-
added.push(Decoration.inline(metadata.from, metadata.to, { class: MLV_EDITOR_AI_STREAMING_CLASS }, { [MLV_EDITOR_AI_REGION_SPEC]: true }));
|
|
154
|
-
}
|
|
155
|
-
if (metadata.chunkFrom < metadata.chunkTo) {
|
|
156
|
-
added.push(Decoration.inline(metadata.chunkFrom, metadata.chunkTo, {
|
|
157
|
-
class: MLV_EDITOR_AI_STREAMING_CHUNK_CLASS,
|
|
158
|
-
}));
|
|
159
|
-
}
|
|
160
|
-
added.push(createCaretDecoration(metadata.to));
|
|
161
|
-
return {
|
|
162
|
-
region: { from: metadata.from, to: metadata.to },
|
|
163
|
-
decorations: decorations.add(transaction.doc, added),
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
if (!value.region || !transaction.docChanged) {
|
|
167
|
-
return decorations === value.decorations
|
|
168
|
-
? value
|
|
169
|
-
: { region: value.region, decorations };
|
|
170
|
-
}
|
|
171
|
-
// Appended transactions from other plugins move the region too; the
|
|
172
|
-
// session re-reads this mapped state after every dispatch.
|
|
173
|
-
return {
|
|
174
|
-
region: {
|
|
175
|
-
from: transaction.mapping.map(value.region.from, -1),
|
|
176
|
-
to: transaction.mapping.map(value.region.to, 1),
|
|
177
|
-
},
|
|
178
|
-
decorations,
|
|
179
|
-
};
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
props: {
|
|
183
|
-
decorations: (state) => MLV_EDITOR_AI_STREAM_KEY.getState(state)?.decorations ?? null,
|
|
184
|
-
handleKeyDown: (_view, event) => {
|
|
185
|
-
if (event.key !== 'Escape')
|
|
186
|
-
return false;
|
|
187
|
-
onEscape();
|
|
188
|
-
return true;
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Runs one AI streaming session against a live Tiptap editor.
|
|
195
|
-
*
|
|
196
|
-
* Framework-free: the runner needs only the editor instance and an async
|
|
197
|
-
* iterable of Markdown/text chunks — no Angular, no provider token, no
|
|
198
|
-
* network. Interim writes render the chunks as plain text inside the target
|
|
199
|
-
* region; the final commit parses the accumulated Markdown through the
|
|
200
|
-
* editor's Markdown manager, so the committed document carries real
|
|
201
|
-
* structure rather than literal Markdown syntax. An editor without a
|
|
202
|
-
* Markdown manager commits plain text — the same fallback the Angular
|
|
203
|
-
* context uses for request serialization. The `'review'` output mode is
|
|
204
|
-
* collect-only: no interim write, no decoration, and no final commit ever
|
|
205
|
-
* touches the document — a `'committed'` result just carries the complete
|
|
206
|
-
* text for the caller to route through the suggestion engine. Every other
|
|
207
|
-
* lifecycle rule below (Escape, cancellation, abandonment, transport and
|
|
208
|
-
* result errors) applies to it unchanged.
|
|
209
|
-
*
|
|
210
|
-
* Lifecycle:
|
|
211
|
-
*
|
|
212
|
-
* - A checkpoint (document JSON) and the target coordinates are captured when
|
|
213
|
-
* the session starts, before the first mutating step.
|
|
214
|
-
* - Chunks are buffered and written at most once per animation frame, and
|
|
215
|
-
* each flush reveals at most {@link MlvEditorAiStreamOptions.revealCharsPerFrame}
|
|
216
|
-
* characters — the remainder carries over to self-scheduled follow-up
|
|
217
|
-
* frames, smoothing bursty providers into a steady typing cadence. Every
|
|
218
|
-
* interim write dispatches with `addToHistory: false`, so no partial state
|
|
219
|
-
* ever becomes an undo step.
|
|
220
|
-
* - Each flush's newly revealed slice carries a one-shot
|
|
221
|
-
* {@link MLV_EDITOR_AI_STREAMING_CHUNK_CLASS} entrance decoration (created
|
|
222
|
-
* once, then only mapped), and an `aria-hidden`
|
|
223
|
-
* {@link MLV_EDITOR_AI_CARET_CLASS} widget marks the insertion tip while
|
|
224
|
-
* the session writes. Both are view-layer decorations: removed when the
|
|
225
|
-
* session settles and excluded from every serialization, exactly like the
|
|
226
|
-
* region tint.
|
|
227
|
-
* - On successful completion the session silently reverts to the checkpoint
|
|
228
|
-
* and commits the complete content as exactly one history-visible step
|
|
229
|
-
* (closed against the previous history group): one undo restores the
|
|
230
|
-
* pre-session document.
|
|
231
|
-
* - {@link MlvEditorAiStreamHandle.cancel} restores the checkpoint and
|
|
232
|
-
* resolves with `'cancelled'` — silently, with no error code. Pressing
|
|
233
|
-
* Escape inside the content region triggers the same cancellation through
|
|
234
|
-
* the session plugin's key handler.
|
|
235
|
-
* - A thrown or rejected chunk iteration restores the checkpoint and resolves
|
|
236
|
-
* `'failed'` with `'ai-transport'`. Empty or whitespace-only total output
|
|
237
|
-
* restores the checkpoint and resolves `'failed'` with `'ai-result'`.
|
|
238
|
-
* - **Abandonment**: any external transaction — one whose root carries no
|
|
239
|
-
* session metadata but changes the document (directly or through appended
|
|
240
|
-
* transactions) — ends the session immediately following the drag-lifecycle
|
|
241
|
-
* precedent. The session stops consuming and writing, removes its
|
|
242
|
-
* decoration, and resolves `'abandoned'` with `error: null` and
|
|
243
|
-
* `aborted: true`. **Nothing is restored**: inverting the checkpoint
|
|
244
|
-
* through foreign steps is unsafe, so the document is left exactly as the
|
|
245
|
-
* external edit made it, including any interim session writes (which were
|
|
246
|
-
* history-invisible and therefore cannot be undone individually).
|
|
247
|
-
* - Destroying the editor mid-session settles it as `'abandoned'` without
|
|
248
|
-
* touching the DOM.
|
|
249
|
-
*
|
|
250
|
-
* The session never throws and {@link MlvEditorAiStreamHandle.done} never
|
|
251
|
-
* rejects. Starting a new session on an editor cancels the session already
|
|
252
|
-
* running there.
|
|
253
|
-
*
|
|
254
|
-
* @param editor Live Tiptap editor the session writes into.
|
|
255
|
-
* @param options Chunk source, output mode, and optional frame scheduler.
|
|
256
|
-
* @returns Handle exposing completion, cancellation, and checkpoint restore.
|
|
257
|
-
*/
|
|
258
|
-
export function runMlvEditorAiStream(editor, options) {
|
|
259
|
-
const scheduler = options.scheduler ?? defaultScheduler;
|
|
260
|
-
const output = options.output;
|
|
261
|
-
const revealCap = normalizeRevealCap(options.revealCharsPerFrame);
|
|
262
|
-
const iterator = options.chunks[Symbol.asyncIterator]();
|
|
263
|
-
let resolveDone;
|
|
264
|
-
const done = new Promise((resolve) => {
|
|
265
|
-
resolveDone = resolve;
|
|
266
|
-
});
|
|
267
|
-
/** Resolves the consumption loop's race when the session settles early. */
|
|
268
|
-
let notifyInterrupt = () => undefined;
|
|
269
|
-
const interrupted = new Promise((resolve) => {
|
|
270
|
-
notifyInterrupt = () => resolve('interrupted');
|
|
271
|
-
});
|
|
272
|
-
let settled = false;
|
|
273
|
-
let hostDestroyed = false;
|
|
274
|
-
let registered = false;
|
|
275
|
-
let mutated = false;
|
|
276
|
-
let accumulated = '';
|
|
277
|
-
/** Characters of `accumulated` already written into the document. */
|
|
278
|
-
let written = 0;
|
|
279
|
-
/**
|
|
280
|
-
* Characters the first (region-bootstrapping) flush wrote. Marks the exact
|
|
281
|
-
* document point where prosemirror-history's deferred mapping pins the
|
|
282
|
-
* `to` positions of earlier undo items (ReplaceStep maps `to` with assoc
|
|
283
|
-
* -1, so later appends never move them past it); the precise revert must
|
|
284
|
-
* split its steps at this point rather than replace across it.
|
|
285
|
-
*/
|
|
286
|
-
let firstWritten = 0;
|
|
287
|
-
let region = null;
|
|
288
|
-
let scheduled = false;
|
|
289
|
-
let cancelScheduled = null;
|
|
290
|
-
/** Politely closes the chunk producer; its cleanup must not break settlement. */
|
|
291
|
-
const closeIterator = () => {
|
|
292
|
-
try {
|
|
293
|
-
const closing = iterator.return?.();
|
|
294
|
-
if (closing)
|
|
295
|
-
void Promise.resolve(closing).catch(() => undefined);
|
|
296
|
-
}
|
|
297
|
-
catch {
|
|
298
|
-
// A producer throwing from return() has no bearing on the session result.
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
if (editor.isDestroyed) {
|
|
302
|
-
settled = true;
|
|
303
|
-
closeIterator();
|
|
304
|
-
resolveDone({ status: 'abandoned', error: null, aborted: true, text: '' });
|
|
305
|
-
return {
|
|
306
|
-
done,
|
|
307
|
-
cancel: () => undefined,
|
|
308
|
-
restoreCheckpoint: () => false,
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
// One session per editor: the shared plugin key must never register twice,
|
|
312
|
-
// and two writers over one region cannot both be correct.
|
|
313
|
-
activeSessions.get(editor)?.();
|
|
314
|
-
// Captured before the first mutating step. External document changes abandon
|
|
315
|
-
// the session, and only session writes mutate otherwise, so these stay valid
|
|
316
|
-
// until the session itself writes.
|
|
317
|
-
const checkpoint = editor.state.doc.toJSON();
|
|
318
|
-
const selectionFrom = editor.state.selection.from;
|
|
319
|
-
const selectionTo = editor.state.selection.to;
|
|
320
|
-
const $to = editor.state.selection.$to;
|
|
321
|
-
/** Gap after the selection's top-level block, where `'insert-below'` writes. */
|
|
322
|
-
const insertAt = $to.depth === 0 ? $to.pos : $to.after(1);
|
|
323
|
-
/** @internal Parses the checkpoint back into a document node. */
|
|
324
|
-
const checkpointDoc = () => ProseMirrorNode.fromJSON(editor.schema, checkpoint);
|
|
325
|
-
/**
|
|
326
|
-
* @internal Tries to revert only the session-written region back to the
|
|
327
|
-
* checkpoint content. Preferred over replacing the whole document because
|
|
328
|
-
* a whole-document replace map collapses every position inside it, which
|
|
329
|
-
* would corrupt the deferred mapping prosemirror-history applies to undo
|
|
330
|
-
* items recorded before the session. Returns whether the built steps are
|
|
331
|
-
* usable; the caller still verifies the result equals the checkpoint.
|
|
332
|
-
*/
|
|
333
|
-
const buildPreciseRevert = (tr, target) => {
|
|
334
|
-
if (!region)
|
|
335
|
-
return false;
|
|
336
|
-
try {
|
|
337
|
-
if (output === 'replace-selection') {
|
|
338
|
-
// Earlier undo items' `to` positions sit pinned at the end of the
|
|
339
|
-
// first interim write (see `firstWritten`). Splitting the revert
|
|
340
|
-
// there keeps every step boundary exactly on that point, so the
|
|
341
|
-
// deferred mapping prosemirror-history applies to pre-session undo
|
|
342
|
-
// items never crosses the interior of a replaced range — which would
|
|
343
|
-
// silently corrupt those items.
|
|
344
|
-
const firstEnd = Math.min(region.from + firstWritten, region.to);
|
|
345
|
-
if (firstEnd < region.to)
|
|
346
|
-
tr.delete(firstEnd, region.to);
|
|
347
|
-
tr.replace(region.from, firstEnd, target.slice(selectionFrom, selectionTo));
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
// The streamed paragraph wraps the region by exactly one position on
|
|
351
|
-
// each side; deleting it removes everything the session inserted.
|
|
352
|
-
tr.delete(Math.max(0, region.from - 1), Math.min(tr.doc.content.size, region.to + 1));
|
|
353
|
-
}
|
|
354
|
-
return true;
|
|
355
|
-
}
|
|
356
|
-
catch {
|
|
357
|
-
return false;
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
/**
|
|
361
|
-
* @internal Restores the checkpoint: precisely when the region-local revert
|
|
362
|
-
* verifiably reproduces it, otherwise by replacing the whole document.
|
|
363
|
-
* History-visible restores close the current history group so they undo on
|
|
364
|
-
* their own; invisible restores also hide from history entirely.
|
|
365
|
-
*/
|
|
366
|
-
const restoreDoc = (historyVisible) => {
|
|
367
|
-
const target = checkpointDoc();
|
|
368
|
-
let tr = editor.state.tr;
|
|
369
|
-
if (!buildPreciseRevert(tr, target) || !tr.doc.eq(target)) {
|
|
370
|
-
tr = editor.state.tr;
|
|
371
|
-
tr.replaceWith(0, tr.doc.content.size, target.content);
|
|
372
|
-
}
|
|
373
|
-
tr.setSelection(TextSelection.near(tr.doc.resolve(Math.min(selectionFrom, tr.doc.content.size))));
|
|
374
|
-
tr.setMeta(MLV_EDITOR_AI_STREAM_KEY, MLV_EDITOR_AI_CLEAR_METADATA);
|
|
375
|
-
if (historyVisible)
|
|
376
|
-
closeHistory(tr);
|
|
377
|
-
else
|
|
378
|
-
tr.setMeta('addToHistory', false);
|
|
379
|
-
editor.view.dispatch(tr);
|
|
380
|
-
};
|
|
381
|
-
/**
|
|
382
|
-
* @internal Commits the streamed Markdown as parsed rich content through
|
|
383
|
-
* the editor's Markdown manager, as one history-visible step against the
|
|
384
|
-
* checkpoint document. A single plain paragraph replaces the selection
|
|
385
|
-
* inline, so a one-line result never splits the surrounding block; every
|
|
386
|
-
* other result lands as the block structure the Markdown describes.
|
|
387
|
-
* Returns `false` — without dispatching a content change — when no manager
|
|
388
|
-
* is registered, parsing fails or yields nothing, or the insertion is
|
|
389
|
-
* refused; the caller then commits plain text instead.
|
|
390
|
-
*/
|
|
391
|
-
const commitParsedMarkdown = (text) => {
|
|
392
|
-
const manager = editor.markdown;
|
|
393
|
-
if (!manager)
|
|
394
|
-
return false;
|
|
395
|
-
let blocks;
|
|
396
|
-
try {
|
|
397
|
-
const parsed = manager.parse(text);
|
|
398
|
-
if (!Array.isArray(parsed.content) || parsed.content.length === 0) {
|
|
399
|
-
return false;
|
|
400
|
-
}
|
|
401
|
-
blocks = parsed.content;
|
|
402
|
-
}
|
|
403
|
-
catch {
|
|
404
|
-
return false;
|
|
405
|
-
}
|
|
406
|
-
const inline = output === 'replace-selection' &&
|
|
407
|
-
blocks.length === 1 &&
|
|
408
|
-
blocks[0].type === 'paragraph'
|
|
409
|
-
? (blocks[0].content ?? [])
|
|
410
|
-
: null;
|
|
411
|
-
const content = inline ?? blocks;
|
|
412
|
-
if (content.length === 0)
|
|
413
|
-
return false;
|
|
414
|
-
try {
|
|
415
|
-
return editor
|
|
416
|
-
.chain()
|
|
417
|
-
.command(({ tr }) => {
|
|
418
|
-
tr.setMeta(MLV_EDITOR_AI_STREAM_KEY, MLV_EDITOR_AI_CLEAR_METADATA);
|
|
419
|
-
closeHistory(tr);
|
|
420
|
-
return true;
|
|
421
|
-
})
|
|
422
|
-
.insertContentAt(output === 'replace-selection'
|
|
423
|
-
? { from: selectionFrom, to: selectionTo }
|
|
424
|
-
: insertAt, content)
|
|
425
|
-
.run();
|
|
426
|
-
}
|
|
427
|
-
catch {
|
|
428
|
-
return false;
|
|
429
|
-
}
|
|
430
|
-
};
|
|
431
|
-
/**
|
|
432
|
-
* @internal Commits the complete streamed text as one history-visible step
|
|
433
|
-
* against the checkpoint document. `closeHistory` prevents the step from
|
|
434
|
-
* merging into a directly preceding user edit's undo group. Markdown
|
|
435
|
-
* structure is committed through {@link commitParsedMarkdown}; the inline
|
|
436
|
-
* path below is the plain-text fallback for editors without a Markdown
|
|
437
|
-
* manager.
|
|
438
|
-
*/
|
|
439
|
-
const commitFinal = (text) => {
|
|
440
|
-
if (commitParsedMarkdown(text))
|
|
441
|
-
return;
|
|
442
|
-
const tr = editor.state.tr;
|
|
443
|
-
let end;
|
|
444
|
-
if (output === 'replace-selection') {
|
|
445
|
-
tr.insertText(text, selectionFrom, selectionTo);
|
|
446
|
-
end = tr.mapping.map(selectionTo, 1);
|
|
447
|
-
}
|
|
448
|
-
else {
|
|
449
|
-
const paragraph = editor.schema.nodes['paragraph'];
|
|
450
|
-
if (paragraph) {
|
|
451
|
-
tr.insert(insertAt, paragraph.create(null, editor.schema.text(text)));
|
|
452
|
-
end = insertAt + 1 + text.length;
|
|
453
|
-
}
|
|
454
|
-
else {
|
|
455
|
-
tr.insertText(text, insertAt, insertAt);
|
|
456
|
-
end = tr.mapping.map(insertAt, 1);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
tr.setSelection(TextSelection.near(tr.doc.resolve(Math.min(end, tr.doc.content.size))));
|
|
460
|
-
tr.setMeta(MLV_EDITOR_AI_STREAM_KEY, MLV_EDITOR_AI_CLEAR_METADATA);
|
|
461
|
-
closeHistory(tr);
|
|
462
|
-
editor.view.dispatch(tr);
|
|
463
|
-
};
|
|
464
|
-
/**
|
|
465
|
-
* @internal Settles the session exactly once: detaches every listener,
|
|
466
|
-
* cancels pending frames, closes the producer, performs the requested
|
|
467
|
-
* restore/commit, unregisters the plugin, and resolves `done`. Wrapped so a
|
|
468
|
-
* failing dispatch can never surface as a thrown error — the session's
|
|
469
|
-
* contract is that it never throws.
|
|
470
|
-
*/
|
|
471
|
-
const settle = (status, error, behavior) => {
|
|
472
|
-
if (settled)
|
|
473
|
-
return;
|
|
474
|
-
settled = true;
|
|
475
|
-
editor.off('transaction', onTransaction);
|
|
476
|
-
editor.off('destroy', onDestroy);
|
|
477
|
-
cancelScheduled?.();
|
|
478
|
-
cancelScheduled = null;
|
|
479
|
-
scheduled = false;
|
|
480
|
-
closeIterator();
|
|
481
|
-
try {
|
|
482
|
-
if (!hostDestroyed && !editor.isDestroyed) {
|
|
483
|
-
if (behavior.restore && mutated)
|
|
484
|
-
restoreDoc(false);
|
|
485
|
-
if (behavior.commit)
|
|
486
|
-
commitFinal(accumulated);
|
|
487
|
-
if (registered)
|
|
488
|
-
editor.unregisterPlugin(MLV_EDITOR_AI_STREAM_KEY);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
catch {
|
|
492
|
-
// Settlement must complete even when a final dispatch fails; the result
|
|
493
|
-
// below still reports the session outcome.
|
|
494
|
-
}
|
|
495
|
-
if (activeSessions.get(editor) === cancel)
|
|
496
|
-
activeSessions.delete(editor);
|
|
497
|
-
notifyInterrupt();
|
|
498
|
-
resolveDone({
|
|
499
|
-
status,
|
|
500
|
-
error,
|
|
501
|
-
aborted: status === 'cancelled' || status === 'abandoned',
|
|
502
|
-
text: accumulated,
|
|
503
|
-
});
|
|
504
|
-
};
|
|
505
|
-
/**
|
|
506
|
-
* @internal Abandons on any external document change: the root transaction
|
|
507
|
-
* carries no session metadata and the document changed, either directly or
|
|
508
|
-
* through transactions other plugins appended to that root. Session-rooted
|
|
509
|
-
* events are skipped wholesale — reactions other plugins append to a
|
|
510
|
-
* session write (for example a trailing paragraph) belong to the session.
|
|
511
|
-
*/
|
|
512
|
-
const onTransaction = ({ transaction, appendedTransactions, }) => {
|
|
513
|
-
if (settled)
|
|
514
|
-
return;
|
|
515
|
-
if (transaction.getMeta(MLV_EDITOR_AI_STREAM_KEY) !== undefined)
|
|
516
|
-
return;
|
|
517
|
-
const docChanged = transaction.docChanged ||
|
|
518
|
-
appendedTransactions.some((appended) => appended.docChanged);
|
|
519
|
-
if (docChanged)
|
|
520
|
-
settle('abandoned', null, { restore: false, commit: false });
|
|
521
|
-
};
|
|
522
|
-
/** @internal A destroyed editor leaves nothing to restore or unregister. */
|
|
523
|
-
const onDestroy = () => {
|
|
524
|
-
hostDestroyed = true;
|
|
525
|
-
settle('abandoned', null, { restore: false, commit: false });
|
|
526
|
-
};
|
|
527
|
-
/**
|
|
528
|
-
* @internal Writes the next paced slice of the accumulated text into the
|
|
529
|
-
* target region. The first write bootstraps the region from the captured
|
|
530
|
-
* target; later writes append at the region's end — appends only, so the
|
|
531
|
-
* per-chunk entrance decorations over earlier text keep their positions and
|
|
532
|
-
* DOM. Each flush reveals at most `revealCap` characters; a remaining
|
|
533
|
-
* buffer schedules its own follow-up frame (carry-over), which is what
|
|
534
|
-
* turns bursty chunks into a steady cadence. Region boundaries are mapped
|
|
535
|
-
* through the transaction and then re-read from plugin state, so appended
|
|
536
|
-
* transactions cannot desynchronize them.
|
|
537
|
-
*/
|
|
538
|
-
const flush = () => {
|
|
539
|
-
if (settled || hostDestroyed || editor.isDestroyed)
|
|
540
|
-
return;
|
|
541
|
-
const pending = accumulated.length - written;
|
|
542
|
-
if (pending <= 0)
|
|
543
|
-
return;
|
|
544
|
-
const count = Math.min(pending, revealCap);
|
|
545
|
-
const delta = accumulated.slice(written, written + count);
|
|
546
|
-
try {
|
|
547
|
-
const tr = editor.state.tr;
|
|
548
|
-
let next;
|
|
549
|
-
let chunkFrom;
|
|
550
|
-
if (region) {
|
|
551
|
-
tr.insertText(delta, region.to, region.to);
|
|
552
|
-
chunkFrom = tr.mapping.map(region.to, -1);
|
|
553
|
-
next = {
|
|
554
|
-
from: tr.mapping.map(region.from, -1),
|
|
555
|
-
to: tr.mapping.map(region.to, 1),
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
else if (output === 'replace-selection') {
|
|
559
|
-
tr.insertText(delta, selectionFrom, selectionTo);
|
|
560
|
-
next = {
|
|
561
|
-
from: tr.mapping.map(selectionFrom, -1),
|
|
562
|
-
to: tr.mapping.map(selectionTo, 1),
|
|
563
|
-
};
|
|
564
|
-
chunkFrom = next.from;
|
|
565
|
-
}
|
|
566
|
-
else {
|
|
567
|
-
const paragraph = editor.schema.nodes['paragraph'];
|
|
568
|
-
if (paragraph) {
|
|
569
|
-
tr.insert(insertAt, paragraph.create(null, editor.schema.text(delta)));
|
|
570
|
-
next = { from: insertAt + 1, to: insertAt + 1 + delta.length };
|
|
571
|
-
}
|
|
572
|
-
else {
|
|
573
|
-
tr.insertText(delta, insertAt, insertAt);
|
|
574
|
-
next = {
|
|
575
|
-
from: tr.mapping.map(insertAt, -1),
|
|
576
|
-
to: tr.mapping.map(insertAt, 1),
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
chunkFrom = next.from;
|
|
580
|
-
}
|
|
581
|
-
tr.setMeta('addToHistory', false);
|
|
582
|
-
tr.setMeta(MLV_EDITOR_AI_STREAM_KEY, {
|
|
583
|
-
type: 'set',
|
|
584
|
-
from: next.from,
|
|
585
|
-
to: next.to,
|
|
586
|
-
chunkFrom,
|
|
587
|
-
chunkTo: next.to,
|
|
588
|
-
});
|
|
589
|
-
editor.view.dispatch(tr);
|
|
590
|
-
mutated = true;
|
|
591
|
-
if (written === 0)
|
|
592
|
-
firstWritten = count;
|
|
593
|
-
written += count;
|
|
594
|
-
region = MLV_EDITOR_AI_STREAM_KEY.getState(editor.state)?.region ?? next;
|
|
595
|
-
// Carry-over: reveal the rest of the buffer on later frames.
|
|
596
|
-
if (written < accumulated.length)
|
|
597
|
-
scheduleFlush();
|
|
598
|
-
}
|
|
599
|
-
catch {
|
|
600
|
-
// The target region rejected the write; the output is unusable here.
|
|
601
|
-
settle('failed', 'ai-result', { restore: true, commit: false });
|
|
602
|
-
}
|
|
603
|
-
};
|
|
604
|
-
/** @internal Schedules at most one flush per frame while chunks arrive. */
|
|
605
|
-
const scheduleFlush = () => {
|
|
606
|
-
if (settled || scheduled)
|
|
607
|
-
return;
|
|
608
|
-
scheduled = true;
|
|
609
|
-
const cancelFrame = scheduler(() => {
|
|
610
|
-
scheduled = false;
|
|
611
|
-
cancelScheduled = null;
|
|
612
|
-
flush();
|
|
613
|
-
});
|
|
614
|
-
// A synchronously invoked scheduler has already flushed by now.
|
|
615
|
-
cancelScheduled = scheduled ? cancelFrame : null;
|
|
616
|
-
};
|
|
617
|
-
const cancel = () => settle('cancelled', null, { restore: true, commit: false });
|
|
618
|
-
const restoreCheckpoint = () => {
|
|
619
|
-
if (hostDestroyed || editor.isDestroyed)
|
|
620
|
-
return false;
|
|
621
|
-
const changed = !editor.state.doc.eq(checkpointDoc());
|
|
622
|
-
if (!settled) {
|
|
623
|
-
cancel();
|
|
624
|
-
return changed;
|
|
625
|
-
}
|
|
626
|
-
if (!changed)
|
|
627
|
-
return false;
|
|
628
|
-
try {
|
|
629
|
-
restoreDoc(true);
|
|
630
|
-
}
|
|
631
|
-
catch {
|
|
632
|
-
return false;
|
|
633
|
-
}
|
|
634
|
-
return true;
|
|
635
|
-
};
|
|
636
|
-
editor.registerPlugin(createStreamPlugin(() => cancel()));
|
|
637
|
-
registered = true;
|
|
638
|
-
editor.on('transaction', onTransaction);
|
|
639
|
-
editor.on('destroy', onDestroy);
|
|
640
|
-
activeSessions.set(editor, cancel);
|
|
641
|
-
void (async () => {
|
|
642
|
-
for (;;) {
|
|
643
|
-
if (settled)
|
|
644
|
-
return;
|
|
645
|
-
const step = await Promise.race([
|
|
646
|
-
interrupted,
|
|
647
|
-
iterator.next().then((result) => result, () => 'thrown'),
|
|
648
|
-
]);
|
|
649
|
-
if (settled || step === 'interrupted')
|
|
650
|
-
return;
|
|
651
|
-
if (step === 'thrown') {
|
|
652
|
-
settle('failed', 'ai-transport', { restore: true, commit: false });
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
if (step.done) {
|
|
656
|
-
if (accumulated.trim().length === 0) {
|
|
657
|
-
settle('failed', 'ai-result', { restore: true, commit: false });
|
|
658
|
-
}
|
|
659
|
-
else {
|
|
660
|
-
// Collect-only review sessions never wrote, so there is nothing to
|
|
661
|
-
// restore (`mutated` stayed false) and nothing to commit.
|
|
662
|
-
settle('committed', null, {
|
|
663
|
-
restore: true,
|
|
664
|
-
commit: output !== 'review',
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
accumulated += step.value;
|
|
670
|
-
// Review sessions only accumulate; a flush would write the document.
|
|
671
|
-
if (output !== 'review')
|
|
672
|
-
scheduleFlush();
|
|
673
|
-
}
|
|
674
|
-
})();
|
|
675
|
-
return { done, cancel, restoreCheckpoint };
|
|
676
|
-
}
|
|
677
|
-
//# sourceMappingURL=editor-ai-stream.js.map
|