@dxos/react-ui-markdown 0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264 → 0.13.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/dist/lib/index.mjs +179 -33
- package/dist/lib/index.mjs.map +1 -1
- package/dist/types/src/MarkdownEditable/MarkdownEditable.d.ts +80 -0
- package/dist/types/src/MarkdownEditable/MarkdownEditable.d.ts.map +1 -0
- package/dist/types/src/MarkdownEditable/MarkdownEditable.stories.d.ts +29 -0
- package/dist/types/src/MarkdownEditable/MarkdownEditable.stories.d.ts.map +1 -0
- package/dist/types/src/MarkdownEditable/index.d.ts +2 -0
- package/dist/types/src/MarkdownEditable/index.d.ts.map +1 -0
- package/dist/types/src/MarkdownStream/MarkdownStream.d.ts +5 -5
- package/dist/types/src/MarkdownStream/MarkdownStream.d.ts.map +1 -1
- package/dist/types/src/MarkdownStream/MarkdownStream.stories.d.ts +2 -2
- package/dist/types/src/MarkdownStream/MarkdownStream.stories.d.ts.map +1 -1
- package/dist/types/src/MarkdownStream/create-controller.d.ts +1 -1
- package/dist/types/src/MarkdownStream/create-controller.d.ts.map +1 -1
- package/dist/types/src/MarkdownStream/index.d.ts +4 -4
- package/dist/types/src/MarkdownStream/index.d.ts.map +1 -1
- package/dist/types/src/MarkdownStream/testing/index.d.ts +1 -1
- package/dist/types/src/MarkdownStream/testing/index.d.ts.map +1 -1
- package/dist/types/src/MarkdownView/MarkdownView.d.ts +16 -5
- package/dist/types/src/MarkdownView/MarkdownView.d.ts.map +1 -1
- package/dist/types/src/MarkdownView/MarkdownView.stories.d.ts +3 -1
- package/dist/types/src/MarkdownView/MarkdownView.stories.d.ts.map +1 -1
- package/dist/types/src/MarkdownView/index.d.ts +1 -1
- package/dist/types/src/MarkdownView/index.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +3 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +27 -26
- package/src/MarkdownEditable/MarkdownEditable.stories.tsx +178 -0
- package/src/MarkdownEditable/MarkdownEditable.tsx +193 -0
- package/src/MarkdownEditable/PERF.md +63 -0
- package/src/MarkdownEditable/index.ts +5 -0
- package/src/MarkdownStream/MarkdownStream.stories.tsx +22 -25
- package/src/MarkdownStream/MarkdownStream.tsx +30 -31
- package/src/MarkdownStream/controller.test.ts +3 -3
- package/src/MarkdownStream/create-controller.ts +7 -7
- package/src/MarkdownStream/index.ts +4 -4
- package/src/MarkdownStream/stream.test.ts +1 -1
- package/src/MarkdownStream/testing/index.ts +1 -1
- package/src/MarkdownView/MarkdownView.stories.tsx +11 -2
- package/src/MarkdownView/MarkdownView.tsx +74 -23
- package/src/MarkdownView/index.ts +1 -1
- package/src/index.ts +3 -2
package/dist/lib/index.mjs
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
1
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
2
2
|
import ReactMarkdown from "react-markdown";
|
|
3
3
|
import remarkGfm from "remark-gfm";
|
|
4
|
-
import { ErrorBoundary, MediaPlayer, useDynamicRef, useStateWithRef, useThemeContext } from "@dxos/react-ui";
|
|
5
4
|
import { SyntaxHighlighter } from "@dxos/react-ui-syntax-highlighter";
|
|
5
|
+
import * as Media from "@dxos/react-ui/Media";
|
|
6
6
|
import { mx } from "@dxos/ui-theme";
|
|
7
7
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { TextEditor, useTextEditor } from "@dxos/react-ui-editor";
|
|
9
|
+
import * as Editable from "@dxos/react-ui/Editable";
|
|
10
|
+
import * as Hooks from "@dxos/react-ui/Hooks";
|
|
11
|
+
import { PROMPT_ELEMENT, crawlerLineEffect, createBasicExtensions, createMarkdownExtensions, createThemeExtensions, createTurnSource, decorateMarkdown, documentSlots, extendedMarkdown, fader, fullWidth, inlineEdit, lineSpacing, navigateNextEffect, navigatePreviousEffect, objectLinks, scroller, turnFolding, typewriter, typewriterBypass, typewriterDrainingEffect, widgetContextEffect, widgetHost, widgetResetEffect, widgetUpdateEffect, xmlBlockDecoration, xmlFormatting, xmlTags } from "@dxos/ui-editor";
|
|
8
12
|
import * as Effect from "effect/Effect";
|
|
9
13
|
import * as Stream from "effect/Stream";
|
|
10
14
|
import { Obj } from "@dxos/echo";
|
|
11
15
|
import { Decoration, EditorView, WidgetType } from "@codemirror/view";
|
|
12
16
|
import * as Queue from "effect/Queue";
|
|
13
17
|
import { addEventListener } from "@dxos/async";
|
|
14
|
-
import
|
|
15
|
-
import { PROMPT_ELEMENT, crawlerLineEffect, createBasicExtensions, createThemeExtensions, createTurnSource, decorateMarkdown, documentSlots, extendedMarkdown, fader, lineSpacing, navigateNextEffect, navigatePreviousEffect, scroller, turnFolding, typewriter, typewriterBypass, typewriterDrainingEffect, xmlBlockDecoration, xmlFormatting, xmlTagContextEffect, xmlTagResetEffect, xmlTagUpdateEffect, xmlTags } from "@dxos/ui-editor";
|
|
18
|
+
import * as EffectEx from "@dxos/effect/EffectEx";
|
|
16
19
|
import { EditorSelection, StateEffect, StateField, Transaction } from "@codemirror/state";
|
|
17
20
|
import * as Fiber from "effect/Fiber";
|
|
18
21
|
import { createPortal } from "react-dom";
|
|
19
|
-
import
|
|
22
|
+
import * as Status from "@dxos/react-ui/Status";
|
|
20
23
|
import { isTruthy } from "@dxos/util";
|
|
21
24
|
import { Domino } from "@dxos/ui";
|
|
22
25
|
//#region src/MarkdownView/MarkdownView.tsx
|
|
@@ -26,20 +29,31 @@ import { Domino } from "@dxos/ui";
|
|
|
26
29
|
* markdown -> remark -> [mdast -> remark plugins] -> [hast -> rehype plugins] -> components -> react elements.
|
|
27
30
|
* Consider using @dxos/react-ui-editor.
|
|
28
31
|
*/
|
|
29
|
-
var MarkdownView = ({ classNames, children, components, content = "" }) => {
|
|
32
|
+
var MarkdownView = ({ classNames, className, children, components, content = "", uniformLineHeight = false, ...props }) => {
|
|
30
33
|
return /* @__PURE__ */ jsxs("div", {
|
|
31
|
-
|
|
34
|
+
...props,
|
|
35
|
+
className: mx(classNames, className),
|
|
32
36
|
children: [/* @__PURE__ */ jsx(ReactMarkdown, {
|
|
33
37
|
remarkPlugins: [remarkGfm],
|
|
34
38
|
skipHtml: true,
|
|
35
39
|
components: {
|
|
36
40
|
...defaultComponents,
|
|
41
|
+
...uniformLineHeight && uniformComponents,
|
|
37
42
|
...components
|
|
38
43
|
},
|
|
39
44
|
children: content
|
|
40
45
|
}), children]
|
|
41
46
|
});
|
|
42
47
|
};
|
|
48
|
+
/** The default link renderer, for a host that overrides `a` for some links and wants the rest as they were. */
|
|
49
|
+
var MarkdownLink = ({ children, href, ...props }) => /* @__PURE__ */ jsx("a", {
|
|
50
|
+
href,
|
|
51
|
+
className: "text-primary-500 hover:text-primary-500",
|
|
52
|
+
target: "_blank",
|
|
53
|
+
rel: "noopener noreferrer",
|
|
54
|
+
...props,
|
|
55
|
+
children
|
|
56
|
+
});
|
|
43
57
|
var defaultComponents = {
|
|
44
58
|
h1: ({ children }) => {
|
|
45
59
|
return /* @__PURE__ */ jsx("h1", {
|
|
@@ -76,29 +90,22 @@ var defaultComponents = {
|
|
|
76
90
|
children
|
|
77
91
|
});
|
|
78
92
|
},
|
|
79
|
-
a: (
|
|
80
|
-
href,
|
|
81
|
-
className: "text-primary-500 hover:text-primary-500",
|
|
82
|
-
target: "_blank",
|
|
83
|
-
rel: "noopener noreferrer",
|
|
84
|
-
...props,
|
|
85
|
-
children
|
|
86
|
-
}),
|
|
93
|
+
a: (props) => /* @__PURE__ */ jsx(MarkdownLink, { ...props }),
|
|
87
94
|
img: ({ src, alt }) => {
|
|
88
95
|
if (!src) return null;
|
|
89
|
-
return /* @__PURE__ */ jsx(
|
|
96
|
+
return /* @__PURE__ */ jsx(Media.Player, {
|
|
90
97
|
src,
|
|
91
98
|
alt,
|
|
92
99
|
classNames: "w-full"
|
|
93
100
|
});
|
|
94
101
|
},
|
|
95
102
|
ol: ({ children, ...props }) => /* @__PURE__ */ jsx("ol", {
|
|
96
|
-
className: "
|
|
103
|
+
className: "ps-6 leading-tight list-decimal",
|
|
97
104
|
...props,
|
|
98
105
|
children
|
|
99
106
|
}),
|
|
100
107
|
ul: ({ children, ...props }) => /* @__PURE__ */ jsx("ul", {
|
|
101
|
-
className: "
|
|
108
|
+
className: "ps-6 leading-tight list-disc",
|
|
102
109
|
...props,
|
|
103
110
|
children
|
|
104
111
|
}),
|
|
@@ -123,6 +130,144 @@ var defaultComponents = {
|
|
|
123
130
|
});
|
|
124
131
|
}
|
|
125
132
|
};
|
|
133
|
+
var uniformHeading = ({ children }) => /* @__PURE__ */ jsx("div", {
|
|
134
|
+
className: "font-medium",
|
|
135
|
+
children
|
|
136
|
+
});
|
|
137
|
+
var uniformComponents = {
|
|
138
|
+
h1: uniformHeading,
|
|
139
|
+
h2: uniformHeading,
|
|
140
|
+
h3: uniformHeading,
|
|
141
|
+
h4: uniformHeading,
|
|
142
|
+
h5: uniformHeading,
|
|
143
|
+
h6: uniformHeading,
|
|
144
|
+
p: ({ children }) => /* @__PURE__ */ jsx("div", { children }),
|
|
145
|
+
blockquote: ({ children }) => /* @__PURE__ */ jsx("blockquote", {
|
|
146
|
+
className: "ps-2 border-l-2 border-accent-text",
|
|
147
|
+
children
|
|
148
|
+
}),
|
|
149
|
+
ul: ({ children }) => /* @__PURE__ */ jsx("ul", {
|
|
150
|
+
className: "ps-5 list-disc",
|
|
151
|
+
children
|
|
152
|
+
}),
|
|
153
|
+
ol: ({ children }) => /* @__PURE__ */ jsx("ol", {
|
|
154
|
+
className: "ps-5 list-decimal",
|
|
155
|
+
children
|
|
156
|
+
}),
|
|
157
|
+
pre: ({ children }) => /* @__PURE__ */ jsx("pre", {
|
|
158
|
+
className: "font-mono whitespace-pre-wrap",
|
|
159
|
+
children
|
|
160
|
+
}),
|
|
161
|
+
code: ({ children }) => /* @__PURE__ */ jsx("code", {
|
|
162
|
+
className: "font-mono text-info-text",
|
|
163
|
+
children
|
|
164
|
+
}),
|
|
165
|
+
table: ({ children }) => /* @__PURE__ */ jsx("table", {
|
|
166
|
+
className: "border-collapse",
|
|
167
|
+
children
|
|
168
|
+
}),
|
|
169
|
+
th: ({ children }) => /* @__PURE__ */ jsx("th", {
|
|
170
|
+
className: "p-0 pe-4 text-start font-medium",
|
|
171
|
+
children
|
|
172
|
+
}),
|
|
173
|
+
td: ({ children }) => /* @__PURE__ */ jsx("td", {
|
|
174
|
+
className: "p-0 pe-4",
|
|
175
|
+
children
|
|
176
|
+
}),
|
|
177
|
+
hr: () => null,
|
|
178
|
+
img: ({ alt }) => alt ? /* @__PURE__ */ jsx("span", { children: alt }) : null
|
|
179
|
+
};
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/MarkdownEditable/MarkdownEditable.tsx
|
|
182
|
+
/**
|
|
183
|
+
* Rendered markdown that becomes a markdown editor in place. Pair it with `editing` held open to
|
|
184
|
+
* make an editor pane, or leave it uncontrolled for click-to-edit.
|
|
185
|
+
*/
|
|
186
|
+
var MarkdownEditable = forwardRef(({ classNames, placeholder, components, readonly, multiline, autoFocus = true, extensions: hostExtensions, ...options }, forwardedRef) => {
|
|
187
|
+
const { value, draft, editing, setDraft, commit, revert, previewProps } = Editable.useEditable({
|
|
188
|
+
...options,
|
|
189
|
+
disabled: options.disabled || readonly
|
|
190
|
+
});
|
|
191
|
+
const handlers = useRef({
|
|
192
|
+
setDraft,
|
|
193
|
+
commit,
|
|
194
|
+
revert
|
|
195
|
+
});
|
|
196
|
+
handlers.current = {
|
|
197
|
+
setDraft,
|
|
198
|
+
commit,
|
|
199
|
+
revert
|
|
200
|
+
};
|
|
201
|
+
const [epoch, setEpoch] = useState(0);
|
|
202
|
+
const discarded = useRef(false);
|
|
203
|
+
const revertAll = useCallback(() => {
|
|
204
|
+
discarded.current = true;
|
|
205
|
+
handlers.current.revert();
|
|
206
|
+
setEpoch((current) => current + 1);
|
|
207
|
+
}, []);
|
|
208
|
+
useImperativeHandle(forwardedRef, () => ({
|
|
209
|
+
commit: () => handlers.current.commit(),
|
|
210
|
+
revert: revertAll
|
|
211
|
+
}), [revertAll]);
|
|
212
|
+
const themeMode = Hooks.useThemeMode();
|
|
213
|
+
const commitOnBlur = options.blurBehavior !== "revert";
|
|
214
|
+
const extensions = useMemo(() => [
|
|
215
|
+
createBasicExtensions({
|
|
216
|
+
lineWrapping: true,
|
|
217
|
+
highlightActiveLine: false,
|
|
218
|
+
placeholder,
|
|
219
|
+
tabbable: true
|
|
220
|
+
}),
|
|
221
|
+
createMarkdownExtensions({ indentWithTab: false }),
|
|
222
|
+
createThemeExtensions({
|
|
223
|
+
themeMode,
|
|
224
|
+
syntaxHighlighting: true,
|
|
225
|
+
slots: fullWidth
|
|
226
|
+
}),
|
|
227
|
+
decorateMarkdown(),
|
|
228
|
+
hostExtensions ?? [],
|
|
229
|
+
inlineEdit({
|
|
230
|
+
onCommit: (text) => {
|
|
231
|
+
if (!discarded.current) handlers.current.commit(text);
|
|
232
|
+
},
|
|
233
|
+
onRevert: () => revertAll(),
|
|
234
|
+
commitOnBlur,
|
|
235
|
+
submitOnEnter: !multiline
|
|
236
|
+
})
|
|
237
|
+
], [
|
|
238
|
+
commitOnBlur,
|
|
239
|
+
multiline,
|
|
240
|
+
placeholder,
|
|
241
|
+
themeMode,
|
|
242
|
+
revertAll,
|
|
243
|
+
hostExtensions
|
|
244
|
+
]);
|
|
245
|
+
if (editing) return /* @__PURE__ */ jsx("div", {
|
|
246
|
+
"data-testid": "markdownEditable.editor",
|
|
247
|
+
className: mx("w-full [&_.cm-content]:!p-0", classNames),
|
|
248
|
+
children: /* @__PURE__ */ jsx(TextEditor, {
|
|
249
|
+
focusable: false,
|
|
250
|
+
initialValue: draft,
|
|
251
|
+
extensions,
|
|
252
|
+
autoFocus,
|
|
253
|
+
selectionEnd: true,
|
|
254
|
+
onChange: (text) => {
|
|
255
|
+
discarded.current = false;
|
|
256
|
+
handlers.current.setDraft(text);
|
|
257
|
+
}
|
|
258
|
+
}, epoch)
|
|
259
|
+
});
|
|
260
|
+
return /* @__PURE__ */ jsx("div", {
|
|
261
|
+
...readonly ? {} : previewProps,
|
|
262
|
+
"data-testid": "markdownEditable.preview",
|
|
263
|
+
className: mx("w-full", !readonly && "cursor-text", !value && placeholder && "text-placeholder", classNames),
|
|
264
|
+
children: /* @__PURE__ */ jsx(MarkdownView, {
|
|
265
|
+
content: value || placeholder,
|
|
266
|
+
components
|
|
267
|
+
})
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
MarkdownEditable.displayName = "MarkdownEditable";
|
|
126
271
|
//#endregion
|
|
127
272
|
//#region src/MarkdownStream/stream.ts
|
|
128
273
|
var renderObjectLink = (obj, block) => `${block ? "!" : ""}[${Obj.getLabel(obj)}](${Obj.getURI(obj)})`;
|
|
@@ -334,12 +479,12 @@ var createMarkdownStreamController = ({ contentRef, viewRef, queueRef, pendingCo
|
|
|
334
479
|
*/
|
|
335
480
|
setContext: (context) => {
|
|
336
481
|
pendingContextRef.current = { value: context };
|
|
337
|
-
viewRef.current?.dispatch({ effects:
|
|
482
|
+
viewRef.current?.dispatch({ effects: widgetContextEffect.of(context) });
|
|
338
483
|
},
|
|
339
484
|
/** Re-applies the last context set, if any. Called once the view exists and after a reset. */
|
|
340
485
|
flushContext: () => {
|
|
341
486
|
const pending = pendingContextRef.current;
|
|
342
|
-
if (pending && viewRef.current) viewRef.current.dispatch({ effects:
|
|
487
|
+
if (pending && viewRef.current) viewRef.current.dispatch({ effects: widgetContextEffect.of(pending.value) });
|
|
343
488
|
},
|
|
344
489
|
/** Reset document. */
|
|
345
490
|
setContent: onReset,
|
|
@@ -353,7 +498,7 @@ var createMarkdownStreamController = ({ contentRef, viewRef, queueRef, pendingCo
|
|
|
353
498
|
},
|
|
354
499
|
/** Update widget state. */
|
|
355
500
|
updateWidget: (id, value) => {
|
|
356
|
-
viewRef.current?.dispatch({ effects:
|
|
501
|
+
viewRef.current?.dispatch({ effects: widgetUpdateEffect.of({
|
|
357
502
|
id,
|
|
358
503
|
value
|
|
359
504
|
}) });
|
|
@@ -468,12 +613,12 @@ var MarkdownStream = forwardRef(({ classNames, debug, content, options, registry
|
|
|
468
613
|
useEffect(() => {
|
|
469
614
|
view?.dispatch({ effects: setFooterVisibleEffect.of(footerVisible) });
|
|
470
615
|
}, [view, footerVisible]);
|
|
471
|
-
const [queue, setQueue, queueRef] = useStateWithRef(Effect.runSync(Queue.unbounded()));
|
|
616
|
+
const [queue, setQueue, queueRef] = Hooks.useStateWithRef(Effect.runSync(Queue.unbounded()));
|
|
472
617
|
const onReset = useCallback(async (text) => {
|
|
473
618
|
contentRef.current = text;
|
|
474
619
|
if (!viewRef.current) return;
|
|
475
620
|
viewRef.current.dispatch({
|
|
476
|
-
effects: [
|
|
621
|
+
effects: [widgetContextEffect.of(pendingContextRef.current?.value ?? null), widgetResetEffect.of(null)],
|
|
477
622
|
changes: [{
|
|
478
623
|
from: 0,
|
|
479
624
|
to: viewRef.current.state.doc.length,
|
|
@@ -498,7 +643,7 @@ var MarkdownStream = forwardRef(({ classNames, debug, content, options, registry
|
|
|
498
643
|
}), [onReset]);
|
|
499
644
|
useEffect(() => {
|
|
500
645
|
const pending = pendingContextRef.current;
|
|
501
|
-
if (view && pending) view.dispatch({ effects:
|
|
646
|
+
if (view && pending) view.dispatch({ effects: widgetContextEffect.of(pending.value) });
|
|
502
647
|
}, [view]);
|
|
503
648
|
useEffect(() => {
|
|
504
649
|
if (!parentRef.current) return;
|
|
@@ -524,9 +669,9 @@ var MarkdownStream = forwardRef(({ classNames, debug, content, options, registry
|
|
|
524
669
|
};
|
|
525
670
|
}, [view]);
|
|
526
671
|
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
|
|
527
|
-
className: mx("dx-
|
|
672
|
+
className: mx("dx-expand", classNames),
|
|
528
673
|
ref: parentRef
|
|
529
|
-
}), /* @__PURE__ */ jsxs(ErrorBoundary, {
|
|
674
|
+
}), /* @__PURE__ */ jsxs(Status.ErrorBoundary, {
|
|
530
675
|
name: "markdown-stream",
|
|
531
676
|
children: [widgets.map(({ Component, root, id, props }) => /* @__PURE__ */ jsx("div", { children: createPortal(/* @__PURE__ */ jsx(Component, {
|
|
532
677
|
view,
|
|
@@ -539,7 +684,7 @@ var turnSource = createTurnSource(PROMPT_ELEMENT);
|
|
|
539
684
|
* Read-only markdown editor configured for streaming (theme, markdown extensions, XML widgets).
|
|
540
685
|
*/
|
|
541
686
|
var useMarkdownStreamTextEditor = (currentContent, { debug, registry, options, extensions: extensionsProp, slots = documentSlots, setFooterRoot }) => {
|
|
542
|
-
const
|
|
687
|
+
const themeMode = Hooks.useThemeMode();
|
|
543
688
|
const [widgets, setWidgets] = useState([]);
|
|
544
689
|
const { view, parentRef } = useTextEditor(() => {
|
|
545
690
|
const content = currentContent.current;
|
|
@@ -560,20 +705,21 @@ var useMarkdownStreamTextEditor = (currentContent, { debug, registry, options, e
|
|
|
560
705
|
xmlFormatting({ skip: debug ? [] : ["prompt"] }),
|
|
561
706
|
!debug && [
|
|
562
707
|
extendedMarkdown({ registry }),
|
|
563
|
-
decorateMarkdown(
|
|
708
|
+
decorateMarkdown(),
|
|
564
709
|
lineSpacing(),
|
|
565
710
|
xmlBlockDecoration({
|
|
566
711
|
tag: "prompt",
|
|
567
|
-
lineClass: "cm-prompt-line cm-prompt-bubble bg-group-surface text-
|
|
712
|
+
lineClass: "cm-prompt-line cm-prompt-bubble bg-group-surface text-fg border-l-[8px] pl-[8px]! pr-2 [&_*]:text-inherit!",
|
|
568
713
|
firstLineClass: "pt-1.5 rounded-t-sm",
|
|
569
714
|
lastLineClass: "pb-1.5 rounded-b-sm",
|
|
570
715
|
hideTags: true
|
|
571
716
|
}),
|
|
572
|
-
|
|
573
|
-
registry,
|
|
717
|
+
widgetHost({
|
|
574
718
|
setWidgets,
|
|
575
719
|
bookmarks: ["prompt"]
|
|
576
720
|
}),
|
|
721
|
+
xmlTags({ registry }),
|
|
722
|
+
objectLinks(),
|
|
577
723
|
turnFolding({ source: turnSource }),
|
|
578
724
|
scroller({
|
|
579
725
|
overScroll: 80,
|
|
@@ -602,7 +748,7 @@ var useMarkdownStreamTextEditor = (currentContent, { debug, registry, options, e
|
|
|
602
748
|
]);
|
|
603
749
|
return {
|
|
604
750
|
view,
|
|
605
|
-
viewRef: useDynamicRef(view),
|
|
751
|
+
viewRef: Hooks.useDynamicRef(view),
|
|
606
752
|
parentRef,
|
|
607
753
|
widgets
|
|
608
754
|
};
|
|
@@ -643,6 +789,6 @@ var useMarkdownStreamQueue = (view, queue, streamerOptions) => {
|
|
|
643
789
|
]);
|
|
644
790
|
};
|
|
645
791
|
//#endregion
|
|
646
|
-
export { MarkdownStream, MarkdownView, computeVisibleRange, createMarkdownStreamController, createStreamer, renderObjectLink, splitFragments, splitSentences, splitSpans, textStream };
|
|
792
|
+
export { MarkdownEditable, MarkdownLink, MarkdownStream, MarkdownView, computeVisibleRange, createMarkdownStreamController, createStreamer, renderObjectLink, splitFragments, splitSentences, splitSpans, textStream };
|
|
647
793
|
|
|
648
794
|
//# sourceMappingURL=index.mjs.map
|
package/dist/lib/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/MarkdownView/MarkdownView.tsx","../../src/MarkdownStream/stream.ts","../../src/MarkdownStream/testing/testing.ts","../../src/MarkdownStream/create-controller.ts","../../src/MarkdownStream/footer.ts","../../src/MarkdownStream/MarkdownStream.tsx"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\nimport React, { type PropsWithChildren } from 'react';\nimport ReactMarkdown, { type Options as ReactMarkdownOptions } from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\n\nimport { MediaPlayer, type ThemedClassName } from '@dxos/react-ui';\nimport { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';\nimport { mx } from '@dxos/ui-theme';\n\nexport type MarkdownViewProps = ThemedClassName<\n PropsWithChildren<{\n content?: string;\n components?: ReactMarkdownOptions['components'];\n }>\n>;\n\n/**\n * Transforms markdown text into react elements.\n * https://github.com/remarkjs/react-markdown\n * markdown -> remark -> [mdast -> remark plugins] -> [hast -> rehype plugins] -> components -> react elements.\n * Consider using @dxos/react-ui-editor.\n */\nexport const MarkdownView = ({ classNames, children, components, content = '' }: MarkdownViewProps) => {\n return (\n <div className={mx(classNames)}>\n <ReactMarkdown remarkPlugins={[remarkGfm]} skipHtml components={{ ...defaultComponents, ...components }}>\n {content}\n </ReactMarkdown>\n {children}\n </div>\n );\n};\n\nconst defaultComponents: ReactMarkdownOptions['components'] = {\n h1: ({ children }) => {\n return <h1 className='pt-1 pb-1 text-accent-text text-xl'>{children}</h1>;\n },\n h2: ({ children }) => {\n return <h2 className='pt-1 pb-1 text-accent-text text-lg'>{children}</h2>;\n },\n h3: ({ children }) => {\n return <h3 className='pt-1 pb-1 text-accent-text text-base'>{children}</h3>;\n },\n h4: ({ children }) => {\n return <h4 className='pt-1 pb-1 uppercase text-base'>{children}</h4>;\n },\n blockquote: ({ children, ...props }) => (\n <blockquote className='my-2 py-2 ps-4 border-l-4 border-accent-text text-accent-text' {...props}>\n {children}\n </blockquote>\n ),\n p: ({ children }) => {\n return <div className='pt-1 pb-1'>{children}</div>;\n },\n a: ({ children, href, ...props }) => (\n <a\n href={href}\n className='text-primary-500 hover:text-primary-500' // TODO(burdon): Use link token.\n target='_blank'\n rel='noopener noreferrer'\n {...props}\n >\n {children}\n </a>\n ),\n // Hide broken images: many markdown sources reference remote URLs that\n // 404 or are blocked. Drop the element on load failure rather than\n // leaving the browser's broken-image placeholder.\n //\n // Media URLs (mp4/mp3/etc. or legacy `iframe`-style embeds) are swapped to a\n // native `<video>` / `<audio>` element by {@link MediaPlayer} so playable\n // media in the source document renders inline.\n img: ({ src, alt }) => {\n if (!src) {\n return null;\n }\n return <MediaPlayer src={src} alt={alt} classNames='w-full' />;\n },\n ol: ({ children, ...props }) => (\n <ol className='pt-1 pb-1 ps-6 leading-tight list-decimal' {...props}>\n {children}\n </ol>\n ),\n ul: ({ children, ...props }) => (\n <ul className='pt-1 pb-1 ps-6 leading-tight list-disc' {...props}>\n {children}\n </ul>\n ),\n li: ({ children, ...props }) => (\n <li className='' {...props}>\n {children}\n </li>\n ),\n pre: ({ children }) => children,\n code: ({ children, className, node }) => {\n const [, language] = /language-(\\w+)/.exec(className || '') || [];\n const inline = !className && node?.position?.start.line === node?.position?.end.line;\n if (inline) {\n return <code className='rounded-xs bg-group-surface px-1 py-0.5 text-sm text-info-text'>{children}</code>;\n }\n\n return (\n <SyntaxHighlighter\n language={language}\n classNames='mt-2 mb-2 p-2 border border-separator rounded-xs text-sm dx-group-surface'\n copyButton\n PreTag='pre'\n >\n {children}\n </SyntaxHighlighter>\n );\n },\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\nimport * as Stream from 'effect/Stream';\n\nimport { Obj } from '@dxos/echo';\n\nexport const renderObjectLink = (obj: Obj.Unknown, block?: boolean) =>\n `${block ? '!' : ''}[${Obj.getLabel(obj)}](${Obj.getURI(obj)})`;\n\nexport type StreamerOptions = {\n /**\n * How to subdivide plain-text spans before emitting them downstream.\n * - `'span'` (default) — keep entire spans intact; one CM dispatch per chunk that arrived at the source.\n * - `'word'` — split text spans at whitespace boundaries (whitespace runs become their own tokens).\n * - `'character'` — split text spans into individual characters.\n * XML/HTML fragments (`<tag>…</tag>`, self-closing tags, hyphenated custom elements) are always\n * emitted as one token regardless of `chunkSize`, otherwise widget mounting would see partial markup.\n */\n chunkSize?: 'character' | 'word' | 'span';\n /**\n * Inter-token delay in ms. Default `0`. Useful for slowing down rapid bursts so the\n * downstream renderer (CodeMirror) can show a visible streaming cadence even when the\n * source emits large chunks at once.\n */\n delayMs?: number;\n};\n\n/**\n * Streams tokens to the consumer, keeping XML/HTML fragments intact.\n *\n * The cadence is controlled by `options.chunkSize`. `'span'` (default) preserves the\n * one-token-per-source-chunk behaviour the function had originally. Use `'word'` or\n * `'character'` to decouple the visible cadence from the source's chunk size — useful when\n * the AI service emits large partial blocks but you want a smoother typewriter effect.\n */\nexport const createStreamer = (\n source: Stream.Stream<string>,\n { chunkSize = 'span', delayMs = 0 }: StreamerOptions = {},\n) => {\n const subdivide =\n chunkSize === 'span'\n ? (token: string) => [token]\n : (token: string) => (isXmlFragment(token) ? [token] : splitTextSpan(token, chunkSize));\n\n let stream: Stream.Stream<string> = source.pipe(\n Stream.flatMap((chunk) => Stream.fromIterable(splitFragments(chunk).flatMap(subdivide))),\n );\n if (delayMs > 0) {\n stream = stream.pipe(Stream.tap(() => Effect.sleep(`${delayMs} millis`)));\n }\n return stream;\n};\n\n/** A token starts with `<` if and only if it is an XML/HTML fragment produced by `splitFragments`. */\nconst isXmlFragment = (token: string): boolean => token.startsWith('<');\n\n/**\n * Subdivide a non-XML text span into smaller tokens. `'word'` splits into runs of non-whitespace\n * and runs of whitespace as separate tokens (so the renderer can display whitespace cadence too).\n */\nconst splitTextSpan = (span: string, chunkSize: 'character' | 'word'): string[] => {\n if (chunkSize === 'character') {\n return [...span];\n }\n return span.match(/\\s+|\\S+/g) ?? [span];\n};\n\n/** Matches opening tag names, including custom elements with hyphens (e.g. dom-widget). */\nconst OPENING_TAG_NAME = /^<([a-zA-Z][\\w-]*)(?:\\s[^>]*)?>/;\n\n/**\n * Splits text into chunks, preserving XML/HTML fragments.\n */\nexport const splitFragments = (text: string): string[] => {\n // First tokenize with tags to get tags as complete tokens.\n const initialTokens = splitSpans(text);\n const tokens: string[] = [];\n\n let i = 0;\n while (i < initialTokens.length) {\n const token = initialTokens[i];\n\n // Check if this is an opening tag.\n if (token.startsWith('<') && !token.startsWith('</') && !token.endsWith('/>')) {\n const tagMatch = token.match(OPENING_TAG_NAME);\n if (tagMatch) {\n const tagName = tagMatch[1];\n const closingTag = `</${tagName}>`;\n\n // Collect tokens until we find the closing tag.\n let fragment = token;\n let foundClosing = false;\n let j = i + 1;\n while (j < initialTokens.length) {\n fragment += initialTokens[j];\n if (initialTokens[j] === closingTag) {\n foundClosing = true;\n break;\n }\n j++;\n }\n\n if (foundClosing) {\n // Return the complete element as one token.\n tokens.push(fragment);\n i = j + 1;\n } else {\n // No closing tag found, just add the opening tag.\n tokens.push(token);\n i++;\n }\n } else {\n // Not a valid opening tag.\n tokens.push(token);\n i++;\n }\n } else {\n // Not an opening tag (could be closing tag, self-closing, or regular character).\n tokens.push(token);\n i++;\n }\n }\n\n return tokens;\n};\n\n// TODO(burdon): Split into paragraphs and tags.\n// export const tokenizeWithTags = (text: string): string[] => {\n// const tokens: string[] = [];\n// let i = 0;\n// while (i < text.length) {\n// if (text[i] === '<') {\n// // Find the closing bracket.\n// const closeIndex = text.indexOf('>', i);\n// if (closeIndex !== -1) {\n// // Include the complete tag.\n// tokens.push(text.slice(i, closeIndex + 1));\n// i = closeIndex + 1;\n// } else {\n// // No closing bracket found, return the entire remaining fragment.\n// tokens.push(text.slice(i));\n// break;\n// }\n// } else {\n// // Regular character.\n// tokens.push(text[i]);\n// i++;\n// }\n// }\n// return tokens;\n// };\n\nexport const splitSpans = (text: string): string[] => {\n const spans: string[] = [];\n let currentText = '';\n\n let i = 0;\n while (i < text.length) {\n if (text[i] === '<') {\n // If we have accumulated text, split it into sentences and push them.\n if (currentText) {\n // const sentences = splitSentences(currentText);\n // spans.push(...sentences);\n spans.push(currentText);\n currentText = '';\n }\n\n // Find the closing bracket.\n const closeIndex = text.indexOf('>', i);\n if (closeIndex !== -1) {\n // Include the complete tag.\n spans.push(text.slice(i, closeIndex + 1));\n i = closeIndex + 1;\n } else {\n // No closing bracket found, treat the rest as text.\n currentText = text.slice(i);\n break;\n }\n } else {\n // Accumulate regular text.\n currentText += text[i];\n i++;\n }\n }\n\n // Push any remaining text split into sentences.\n if (currentText) {\n spans.push(currentText);\n // const sentences = splitSentences(currentText);\n // spans.push(...sentences);\n }\n\n return spans;\n};\n\n/**\n * Split text into sentences, preserving the sentence-ending punctuation.\n */\nexport const splitSentences = (text: string): string[] => {\n // Match sentences ending with ., !, or ? followed by space or end of string.\n // This regex captures the sentence including its ending punctuation.\n const sentenceRegex = /[^.!?]*[.!?]+(?:\\s+|$)/g;\n const sentences: string[] = [];\n let lastIndex = 0;\n let match;\n\n while ((match = sentenceRegex.exec(text)) !== null) {\n sentences.push(match[0]);\n lastIndex = match.index + match[0].length;\n }\n\n // If there's remaining text that doesn't end with punctuation, include it.\n if (lastIndex < text.length) {\n const remaining = text.slice(lastIndex);\n if (remaining) {\n sentences.push(remaining);\n }\n }\n\n // If no sentences were found, return the original text.\n if (sentences.length === 0 && text) {\n sentences.push(text);\n }\n\n return sentences;\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\n/**\n * Options for the streaming text generator.\n */\nexport type TextStreamOptions = {\n /** Delay between chunks in ms. */\n chunkDelay?: number;\n /** Variance in timing (0-1). */\n variance?: number;\n /** Number of words per chunk. */\n wordsPerChunk?: number;\n};\n\n/**\n * Simulates word-by-word streaming (more natural for LLMs).\n */\nexport async function* textStream(\n text: string,\n options: TextStreamOptions = {},\n): AsyncGenerator<string, void, unknown> {\n const { chunkDelay = 100, variance = 0.3, wordsPerChunk = 3 } = options;\n\n // Split into words while preserving whitespace.\n const words = text.match(/\\S+|\\s+/g) || [];\n\n let i = 0;\n while (i < words.length) {\n // Collect multiple words for this chunk.\n const chunkWords: string[] = [];\n\n // Add words up to wordsPerChunk (counting only non-whitespace as words).\n let wordCount = 0;\n while (i < words.length && wordCount < wordsPerChunk) {\n const word = words[i];\n chunkWords.push(word);\n\n // Only count non-whitespace as words.\n if (word.trim()) {\n wordCount++;\n }\n i++;\n }\n\n // Yield the chunk.\n const chunk = chunkWords.join('');\n yield chunk;\n\n // Calculate delay based on chunk length.\n const varianceMultiplier = 1 + (Math.random() - 0.5) * variance * 2;\n const delay = chunkDelay * varianceMultiplier;\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n}\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { EditorView } from '@codemirror/view';\nimport * as Queue from 'effect/Queue';\nimport { type RefObject } from 'react';\n\nimport { addEventListener } from '@dxos/async';\nimport { EffectEx } from '@dxos/effect';\nimport {\n crawlerLineEffect,\n navigateNextEffect,\n navigatePreviousEffect,\n xmlTagContextEffect,\n xmlTagUpdateEffect,\n} from '@dxos/ui-editor';\n\nimport { type DocumentRange, type MarkdownStreamController } from './MarkdownStream';\n\n// Kept out of `MarkdownStream.tsx`: react-refresh only fast-refreshes a module whose\n// exports are all components, so values exported beside them force a full page reload on\n// every edit.\n\n/**\n * External controller API.\n *\n * @internal Exported for tests via `@dxos/react-ui-markdown/testing`; hosts get this through the\n * component's ref.\n */\nexport const computeVisibleRange = (view: EditorView): DocumentRange => {\n const rect = view.scrollDOM.getBoundingClientRect();\n // `posAtCoords(_, false)` clamps to the nearest position rather than returning null.\n const from = view.posAtCoords({ x: rect.left + 1, y: rect.top + 1 }, false);\n const to = view.posAtCoords({ x: rect.left + 1, y: rect.bottom - 1 }, false);\n return { from, to };\n};\n\nexport type MarkdownStreamControllerDeps = {\n contentRef: RefObject<string | undefined>;\n viewRef: RefObject<EditorView | null>;\n queueRef: RefObject<Queue.Queue<string>>;\n /**\n * Holds a context set before the view existed, for {@link flushContext} to apply once it does.\n * Also re-applied after a document reset, which rebuilds decorations from a state without it.\n */\n pendingContextRef: RefObject<{ value: any } | undefined>;\n onReset: (text: string) => Promise<void>;\n};\n\nexport const createMarkdownStreamController = ({\n contentRef,\n viewRef,\n queueRef,\n pendingContextRef,\n onReset,\n}: MarkdownStreamControllerDeps): MarkdownStreamController => {\n return {\n get length() {\n return viewRef.current?.state.doc.length;\n },\n\n /** Focus the editor. */\n focus: () => {\n viewRef.current?.focus();\n },\n\n /** Scroll to bottom. */\n scrollToBottom: (behavior?: ScrollBehavior) => {\n viewRef.current?.dispatch({\n effects: crawlerLineEffect.of({ line: -1, behavior }),\n });\n },\n\n /** Scroll the given document position into view. */\n scrollTo: (pos: number, options?: { y?: 'start' | 'center' | 'end' | 'nearest' }) => {\n const view = viewRef.current;\n if (!view) {\n return;\n }\n\n const clamped = Math.max(0, Math.min(pos, view.state.doc.length));\n view.dispatch({ effects: EditorView.scrollIntoView(clamped, { y: options?.y ?? 'start' }) });\n },\n\n /** The document range currently visible in the viewport. */\n getVisibleRange: () => {\n const view = viewRef.current;\n return view ? computeVisibleRange(view) : undefined;\n },\n\n /** Subscribe to visible-range changes (scroll). Fires immediately with the current range. */\n onVisibleRangeChange: (cb: (range: DocumentRange) => void) => {\n const view = viewRef.current;\n if (!view) {\n return () => {};\n }\n const handler = () => cb(computeVisibleRange(view));\n handler();\n return addEventListener(view.scrollDOM, 'scroll', handler, { passive: true });\n },\n\n /** Navigate previous prompt. */\n navigatePrevious: () => {\n viewRef.current?.dispatch({\n effects: navigatePreviousEffect.of(),\n });\n },\n\n /** Navigate next prompt. */\n navigateNext: () => {\n viewRef.current?.dispatch({\n effects: navigateNextEffect.of(),\n });\n },\n\n /**\n * Set the context for widgets (XML tags).\n *\n * Remembered as well as dispatched: a host has no signal for when the view exists, and the\n * dispatch is a no-op before it does — so without this a context set on mount is silently lost and\n * every widget callback through it dies on an optional call. {@link flushContext} re-applies it.\n */\n setContext: (context: any) => {\n pendingContextRef.current = { value: context };\n viewRef.current?.dispatch({\n effects: xmlTagContextEffect.of(context),\n });\n },\n\n /** Re-applies the last context set, if any. Called once the view exists and after a reset. */\n flushContext: () => {\n const pending = pendingContextRef.current;\n if (pending && viewRef.current) {\n viewRef.current.dispatch({\n effects: xmlTagContextEffect.of(pending.value),\n });\n }\n },\n\n /** Reset document. */\n setContent: onReset,\n\n /** Append to queue (and stream). */\n append: async (text: string) => {\n contentRef.current += text;\n if (text.length) {\n // Always go through the streaming queue, even when the doc starts empty. Skipping the\n // queue in that case (via `onReset`) bypasses the `typewriter` extension's transaction filter\n // and the first chunk lands in one CM dispatch — defeating the typewriter for any\n // consumer (e.g. ChatThread) where the first delta is large because upstream batching\n // collected several streaming partials before React rendered.\n const queue = queueRef.current;\n if (queue) {\n await EffectEx.runAndForwardErrors(Queue.offer(queue, text));\n }\n }\n },\n\n /** Update widget state. */\n updateWidget: (id: string, value: any) => {\n viewRef.current?.dispatch({\n effects: xmlTagUpdateEffect.of({ id, value }),\n });\n },\n } satisfies MarkdownStreamController;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { type Extension, StateEffect, StateField } from '@codemirror/state';\nimport { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';\n\nimport { Domino } from '@dxos/ui';\nimport { typewriterDrainingEffect } from '@dxos/ui-editor';\n\n/**\n * Host-controlled visibility intent. The footer block widget is rendered only when this is\n * `true` AND typewriter is not actively draining its typewriter buffer — the latter is observed\n * through {@link typewriterDrainingEffect}. Removing the decoration during a drip avoids the\n * scroll-measure conflict between CM's view-line model and the floating absolute child.\n */\nexport const setFooterVisibleEffect = StateEffect.define<boolean>();\n\ntype FooterState = {\n /** Host wants the footer rendered. */\n wanted: boolean;\n /** Typewriter is actively dripping into the document. */\n draining: boolean;\n decorations: DecorationSet;\n};\n\n/**\n * Renders a host-supplied React subtree as a CodeMirror block widget anchored at\n * `doc.length`. The widget lives inside the document, so it scrolls with content.\n *\n * Toggle the host-intent visibility via {@link setFooterVisibleEffect}. The widget is also\n * automatically removed while {@link typewriterDrainingEffect} reports `true` and re-mounted\n * once the typewriter's buffer drains — this prevents the block widget from interfering with CM's\n * view-line measurement during the typewriter drip.\n *\n * For pure doc edits (no visibility transition), the decoration's position is mapped\n * through the change set so insertions at the end translate the anchor without destroying\n * the widget's DOM.\n */\nexport const footer = (setRoot: (el: HTMLElement | null) => void): Extension => {\n const widget = new FooterWidget(setRoot);\n const buildSet = (length: number): DecorationSet =>\n Decoration.set([Decoration.widget({ widget, block: true, side: 1 }).range(length)]);\n\n const field = StateField.define<FooterState>({\n create: () => ({ wanted: false, draining: false, decorations: Decoration.none }),\n update: (state, tr) => {\n let { wanted, draining, decorations } = state;\n for (const effect of tr.effects) {\n if (effect.is(setFooterVisibleEffect)) {\n wanted = effect.value;\n }\n if (effect.is(typewriterDrainingEffect)) {\n draining = effect.value;\n }\n }\n\n // Also gate on the document being non-empty: there's nothing for the footer to anchor\n // below, and rendering it on a blank doc looks like detached chrome.\n const docLength = tr.state.doc.length;\n const visible = wanted && !draining && docLength > 0;\n const wasVisible = decorations.size > 0;\n if (visible !== wasVisible) {\n decorations = visible ? buildSet(docLength) : Decoration.none;\n } else if (tr.docChanged && decorations.size > 0) {\n // Position-map the existing decoration so insertions at the end translate the\n // widget anchor without destroying the DOM (`widget.eq` is identity-true).\n decorations = decorations.map(tr.changes);\n }\n\n return { wanted, draining, decorations };\n },\n provide: (f) => EditorView.decorations.from(f, (state) => state.decorations),\n });\n\n return [field];\n};\n\n/**\n * Block widget rendered at the end of the document. The DOM element is reported via\n * `setRoot` so the host React component can `createPortal` arbitrary content into it.\n */\nclass FooterWidget extends WidgetType {\n constructor(private readonly _setRoot: (el: HTMLElement | null) => void) {\n super();\n }\n\n // Singleton equality so CM keeps the same DOM element across decoration rebuilds —\n // the React subtree portaled into it is not unmounted on every doc change.\n override eq(_other: this): boolean {\n return true;\n }\n\n override ignoreEvent(): boolean {\n return true;\n }\n\n override toDOM(): HTMLElement {\n // The outer block-widget element is `position: relative` with zero flow height so it\n // does not push the document layout (autoScroll, line measurement, etc. ignore it).\n // The inner element is `position: absolute`, taking the React subtree out of flow — it\n // renders as a floating layer anchored to the doc tail without consuming space.\n const inner = Domino.of('div')\n .classNames('cm-stream-footer-content')\n .style({ position: 'absolute', left: '0', top: '0' });\n\n const el = Domino.of('div')\n .classNames('cm-stream-footer')\n .style({ position: 'relative', height: '0' })\n .append(inner);\n\n this._setRoot(inner.root);\n return el.root;\n }\n\n override destroy(): void {\n this._setRoot(null);\n }\n}\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { EditorSelection, type Extension, Transaction } from '@codemirror/state';\nimport { EditorView } from '@codemirror/view';\nimport * as Effect from 'effect/Effect';\nimport * as Fiber from 'effect/Fiber';\nimport * as Queue from 'effect/Queue';\nimport * as Stream from 'effect/Stream';\nimport React, {\n type ReactNode,\n type RefObject,\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n} from 'react';\nimport { createPortal } from 'react-dom';\n\nimport { addEventListener } from '@dxos/async';\nimport { EffectEx } from '@dxos/effect';\nimport { ErrorBoundary, type ThemedClassName, useDynamicRef, useStateWithRef, useThemeContext } from '@dxos/react-ui';\nimport { type UseTextEditor, useTextEditor } from '@dxos/react-ui-editor';\nimport {\n type AutoScrollProps,\n PROMPT_ELEMENT,\n ThemeExtensionsOptions,\n type XmlTagsOptions,\n type XmlWidgetState,\n type XmlWidgetStateManager,\n createBasicExtensions,\n createThemeExtensions,\n createTurnSource,\n decorateMarkdown,\n documentSlots,\n extendedMarkdown,\n fader,\n lineSpacing,\n scroller,\n turnFolding,\n typewriter,\n typewriterBypass,\n xmlBlockDecoration,\n xmlFormatting,\n xmlTagContextEffect,\n xmlTagResetEffect,\n xmlTags,\n} from '@dxos/ui-editor';\nimport { mx } from '@dxos/ui-theme';\nimport { isTruthy } from '@dxos/util';\n\nimport { createMarkdownStreamController } from './create-controller';\nimport { footer, setFooterVisibleEffect } from './footer';\nimport { type StreamerOptions, createStreamer } from './stream';\n\n/** Document offset range (CodeMirror positions). */\nexport type DocumentRange = { from: number; to: number };\n\nexport interface MarkdownStreamController extends XmlWidgetStateManager {\n get length(): number | undefined;\n focus: () => void;\n scrollToBottom: (behavior?: ScrollBehavior) => void;\n /** Scroll the given document position into view. */\n scrollTo: (pos: number, options?: { y?: 'start' | 'center' | 'end' | 'nearest' }) => void;\n /** The document range currently visible in the viewport, or `undefined` if not mounted. */\n getVisibleRange: () => DocumentRange | undefined;\n /** Subscribe to visible-range changes (fires immediately with the current range). Returns an unsubscribe fn. */\n onVisibleRangeChange: (cb: (range: DocumentRange) => void) => () => void;\n navigateNext: () => void;\n navigatePrevious: () => void;\n setContext: (context: any) => void;\n /** Re-applies the last context set. Idempotent; a no-op when none was set or the view is absent. */\n flushContext: () => void;\n setContent: (text: string) => Promise<void>;\n append: (text: string) => Promise<void>;\n}\n\n/** Map the scroll container's top/bottom edges to document positions. */\n\nexport type MarkdownStreamEvent = {\n type: 'submit';\n value: string | null;\n};\n\nexport type MarkdownStreamProps = ThemedClassName<\n {\n debug?: boolean;\n\n /** Initial content. */\n content?: string;\n\n /** View options. */\n options?: {\n autoScroll?: boolean;\n typewriter?: boolean;\n cursor?: boolean;\n fader?: boolean;\n\n /**\n * Streaming cadence. See {@link StreamerOptions}.\n * Use `'word'` or `'character'` to break large source chunks into smaller CM dispatches —\n * useful when the AI service emits big partial blocks but you want a smoother typewriter\n * effect. Combine with `streamDelayMs` to add a per-token sleep.\n * Default: `'span'` (one CM dispatch per source chunk; current behaviour).\n */\n streamCadence?: StreamerOptions['chunkSize'];\n\n /**\n * Per-token delay (ms) for the streaming queue. Default `0`.\n */\n streamDelayMs?: number;\n };\n\n /**\n * Optional React subtree rendered as a CodeMirror block-widget decoration anchored at\n * `doc.length`. Scrolls with the document content (lives inside `cm-content`'s flow).\n * Visibility tracks the truthiness of the prop.\n */\n footer?: ReactNode;\n\n /**\n * Extra CodeMirror extensions appended after the built-in stack — use for keymaps or\n * other host-level behaviour (e.g. `Mod-d` to toggle debug) that should apply when the\n * document is focused.\n */\n extensions?: Extension;\n\n /**\n * Theme extensions.\n */\n slots?: ThemeExtensionsOptions['slots'];\n\n /** Event handler. */\n onEvent?: (event: MarkdownStreamEvent) => void;\n } & (XmlTagsOptions & AutoScrollProps)\n>;\n\n/**\n * Codemirror-based markdown editor with xml tag widtgets and streaming support.\n */\nexport const MarkdownStream = forwardRef<MarkdownStreamController | null, MarkdownStreamProps>(\n ({ classNames, debug, content, options, registry, extensions, footer, slots, onEvent }, forwardedRef) => {\n // Store current content so that we can toggle debug mode. Default to '' so the\n // `append()` path (which does `contentRef.current += text`) doesn't concatenate\n // against `undefined` and stamp `\"undefined\"` into the transcript snapshot.\n const contentRef = useRef(content ?? '');\n\n // DOM node for the footer block widget — populated when its decoration mounts.\n const [footerRoot, setFooterRoot] = useState<HTMLElement | null>(null);\n\n // Survives a context set before the view exists; flushed once it does (see `flushContext`).\n const pendingContextRef = useRef<{ value: any } | undefined>(undefined);\n\n // Codemirror editor.\n const { parentRef, view, viewRef, widgets } = useMarkdownStreamTextEditor(contentRef, {\n slots,\n debug,\n registry,\n options,\n extensions,\n setFooterRoot: footer ? setFooterRoot : undefined,\n });\n\n // Show the status footer.\n const footerVisible = !!footer;\n useEffect(() => {\n view?.dispatch({ effects: setFooterVisibleEffect.of(footerVisible) });\n }, [view, footerVisible]);\n\n // Streaming text queue.\n const [queue, setQueue, queueRef] = useStateWithRef(Effect.runSync(Queue.unbounded<string>()));\n\n // Reset document.\n const onReset = useCallback(\n async (text: string) => {\n contentRef.current = text;\n if (!viewRef.current) {\n return;\n }\n\n // Set content and scroll to bottom. Re-assert the host context rather than clearing it: it\n // belongs to the host, not the document, so nulling it here would strand every widget in the\n // replacement document with `context: undefined`.\n viewRef.current.dispatch({\n effects: [xmlTagContextEffect.of(pendingContextRef.current?.value ?? null), xmlTagResetEffect.of(null)],\n changes: [{ from: 0, to: viewRef.current.state.doc.length, insert: text }],\n annotations: typewriterBypass.of(true),\n selection: EditorSelection.cursor(text.length),\n });\n\n // New queue.\n setQueue(Effect.runSync(Queue.unbounded<string>()));\n },\n [contentRef, viewRef, setQueue, pendingContextRef],\n );\n\n // Controller API.\n useImperativeHandle(\n forwardedRef,\n () => createMarkdownStreamController({ contentRef, viewRef, queueRef, onReset, pendingContextRef }),\n [onReset],\n );\n\n // Apply a context that was set before the view existed — the dispatch in `setContext` is a no-op\n // until then, and a host mounting the controller has no way to know.\n useEffect(() => {\n const pending = pendingContextRef.current;\n if (view && pending) {\n view.dispatch({ effects: xmlTagContextEffect.of(pending.value) });\n }\n }, [view]);\n\n // Widget events.\n useEffect(() => {\n if (!parentRef.current) {\n return;\n }\n\n // TODO(burdon): Replace this hack with a custom event listener from widgets.\n return addEventListener(parentRef.current, 'click', (event) => {\n const button = (event.target as HTMLElement).closest('[data-action=\"submit\"]');\n if (button?.getAttribute('data-action') === 'submit') {\n onEvent?.({\n type: 'submit',\n value: button.getAttribute('data-value'),\n });\n }\n });\n }, [view, parentRef, onEvent]);\n\n // Consume queue and update document.\n useMarkdownStreamQueue(view, queue, {\n chunkSize: options?.streamCadence,\n delayMs: options?.streamDelayMs,\n });\n\n // Cleanup.\n useEffect(() => {\n return () => {\n view?.destroy();\n };\n }, [view]);\n\n return (\n <>\n {/* Markdown editor. */}\n <div className={mx('dx-container', classNames)} ref={parentRef} />\n\n {/* React widgets are rendered in portals outside of the editor. */}\n <ErrorBoundary name='markdown-stream'>\n {widgets.map(({ Component, root, id, props }) => (\n <div key={id}>{createPortal(<Component view={view} {...props} />, root)}</div>\n ))}\n {footerRoot && footerVisible && createPortal(footer, footerRoot)}\n </ErrorBoundary>\n </>\n );\n },\n);\n\n// Fold each agent response beneath its `<prompt>` (the head element rendered by `xmlBlockDecoration`).\nconst turnSource = createTurnSource(PROMPT_ELEMENT);\n\ntype MarkdownStreamTextEditorParams = Pick<MarkdownStreamProps, 'debug' | 'registry' | 'options' | 'extensions'> &\n Pick<ThemeExtensionsOptions, 'slots'> & {\n setFooterRoot?: (el: HTMLElement | null) => void;\n };\n\ntype MarkdownStreamTextEditorResult = UseTextEditor & {\n viewRef: RefObject<EditorView | null>;\n widgets: XmlWidgetState[];\n};\n\n/**\n * Read-only markdown editor configured for streaming (theme, markdown extensions, XML widgets).\n */\nconst useMarkdownStreamTextEditor = (\n currentContent: RefObject<string | undefined>,\n {\n debug,\n registry,\n options,\n extensions: extensionsProp,\n slots = documentSlots,\n setFooterRoot,\n }: MarkdownStreamTextEditorParams,\n): MarkdownStreamTextEditorResult => {\n const { themeMode } = useThemeContext();\n\n // Active widgets.\n const [widgets, setWidgets] = useState<XmlWidgetState[]>([]);\n\n // Editor.\n const { view, parentRef } = useTextEditor(() => {\n const content = currentContent.current;\n return {\n initialValue: content,\n selection: EditorSelection.cursor(content?.length ?? 0),\n extensions: [\n createBasicExtensions({ lineWrapping: true, readOnly: true }),\n createThemeExtensions({ slots, scrollbarThin: true, syntaxHighlighting: true, themeMode }),\n xmlFormatting({ skip: debug ? [] : ['prompt'] }),\n !debug &&\n [\n extendedMarkdown({ registry }),\n decorateMarkdown({\n // xmlTags extension will handle `dxn:`/`echo:` links/images.\n skip: (node) =>\n (node.name === 'Link' || node.name === 'Image') &&\n (node.url.startsWith('dxn:') || node.url.startsWith('echo:')),\n }),\n // TODO(burdon): Make optional; Removes need for '\\n\\n'.\n lineSpacing(),\n xmlBlockDecoration({\n tag: 'prompt',\n lineClass:\n 'cm-prompt-line cm-prompt-bubble bg-group-surface text-base-fg border-l-[8px] pl-[8px]! pr-2 [&_*]:text-inherit!',\n firstLineClass: 'pt-1.5 rounded-t-sm',\n lastLineClass: 'pb-1.5 rounded-b-sm',\n hideTags: true,\n }),\n xmlTags({ registry, setWidgets, bookmarks: ['prompt'] }),\n // TODO(burdon): Folding gets progressively off due to some widgets?\n turnFolding({ source: turnSource }),\n scroller({ overScroll: 80, autoScroll: options?.autoScroll }),\n options?.typewriter &&\n typewriter({\n cursor: options?.cursor,\n streamingTags: new Set(\n Object.entries(registry ?? {})\n .filter(([, def]) => def.streaming)\n .map(([tag]) => tag),\n ),\n }),\n options?.fader && fader(),\n setFooterRoot && footer(setFooterRoot),\n ].filter(isTruthy),\n extensionsProp,\n ].filter(isTruthy),\n };\n }, [\n themeMode,\n registry,\n debug,\n options?.autoScroll,\n options?.typewriter,\n options?.cursor,\n options?.fader,\n slots,\n extensionsProp,\n ]);\n\n const viewRef = useDynamicRef(view);\n return { view, viewRef, parentRef, widgets };\n};\n\n/**\n * Consumes streaming text from the queue and appends it to the editor document.\n */\nconst useMarkdownStreamQueue = (\n view: EditorView | null,\n queue: Queue.Queue<string>,\n streamerOptions?: StreamerOptions,\n) => {\n const chunkSize = streamerOptions?.chunkSize;\n const delayMs = streamerOptions?.delayMs;\n useEffect(() => {\n if (!view) {\n return;\n }\n\n // Consume queue and update document.\n const fork = Stream.fromQueue(queue).pipe(\n (source) => createStreamer(source, { chunkSize, delayMs }),\n Stream.runForEach((text) =>\n Effect.sync(() => {\n const scrollTop = view.scrollDOM.scrollTop;\n view.dispatch({\n changes: [{ from: view.state.doc.length, insert: text }],\n annotations: Transaction.remote.of(true),\n scrollIntoView: false,\n });\n\n // Prevent autoscrolling.\n requestAnimationFrame(() => {\n view.scrollDOM.scrollTop = scrollTop;\n });\n }),\n ),\n Effect.runFork,\n );\n\n return () => {\n void EffectEx.runAndForwardErrors(Fiber.interrupt(fork));\n };\n }, [view, queue, chunkSize, delayMs]);\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/MarkdownView/MarkdownView.tsx","../../src/MarkdownEditable/MarkdownEditable.tsx","../../src/MarkdownStream/stream.ts","../../src/MarkdownStream/testing/testing.ts","../../src/MarkdownStream/create-controller.ts","../../src/MarkdownStream/footer.ts","../../src/MarkdownStream/MarkdownStream.tsx"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\nimport React, { type ComponentProps, type ComponentPropsWithRef, type PropsWithChildren } from 'react';\nimport ReactMarkdown, { type Options as ReactMarkdownOptions } from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\n\nimport { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';\nimport * as Media from '@dxos/react-ui/Media';\nimport type * as Util from '@dxos/react-ui/Util';\nimport { mx } from '@dxos/ui-theme';\n\nexport type MarkdownViewProps = Util.ThemedClassName<\n ComponentPropsWithRef<'div'> & {\n content?: string;\n components?: ReactMarkdownOptions['components'];\n /**\n * Render every block — headings, quotes, lists, code, tables — at the container's font size and\n * line height with no vertical padding or margin, so each line is the same height. For a\n * clamped preview (`line-clamp-*`), which otherwise cuts partway into a line.\n */\n uniformLineHeight?: boolean;\n }\n> & {\n /** Merged by a parent rendering this `asChild`; consumers use `classNames`. */\n className?: string;\n};\n\n/**\n * Transforms markdown text into react elements.\n * https://github.com/remarkjs/react-markdown\n * markdown -> remark -> [mdast -> remark plugins] -> [hast -> rehype plugins] -> components -> react elements.\n * Consider using @dxos/react-ui-editor.\n */\n// Spreads the rest so a parent rendering it `asChild` (a fieldset's helper text) lands its attributes here.\nexport const MarkdownView = ({\n classNames,\n className,\n children,\n components,\n content = '',\n uniformLineHeight = false,\n ...props\n}: MarkdownViewProps) => {\n return (\n <div {...props} className={mx(classNames, className)}>\n <ReactMarkdown\n remarkPlugins={[remarkGfm]}\n skipHtml\n components={{ ...defaultComponents, ...(uniformLineHeight && uniformComponents), ...components }}\n >\n {content}\n </ReactMarkdown>\n {children}\n </div>\n );\n};\n\n/** The default link renderer, for a host that overrides `a` for some links and wants the rest as they were. */\nexport const MarkdownLink = ({ children, href, ...props }: ComponentProps<'a'>) => (\n <a\n href={href}\n className='text-primary-500 hover:text-primary-500' // TODO(burdon): Use link token.\n target='_blank'\n rel='noopener noreferrer'\n {...props}\n >\n {children}\n </a>\n);\n\nconst defaultComponents: ReactMarkdownOptions['components'] = {\n h1: ({ children }) => {\n return <h1 className='pt-1 pb-1 text-accent-text text-xl'>{children}</h1>;\n },\n h2: ({ children }) => {\n return <h2 className='pt-1 pb-1 text-accent-text text-lg'>{children}</h2>;\n },\n h3: ({ children }) => {\n return <h3 className='pt-1 pb-1 text-accent-text text-base'>{children}</h3>;\n },\n h4: ({ children }) => {\n return <h4 className='pt-1 pb-1 uppercase text-base'>{children}</h4>;\n },\n blockquote: ({ children, ...props }) => (\n <blockquote className='my-2 py-2 ps-4 border-l-4 border-accent-text text-accent-text' {...props}>\n {children}\n </blockquote>\n ),\n p: ({ children }) => {\n return <div className='pt-1 pb-1'>{children}</div>;\n },\n a: (props) => <MarkdownLink {...props} />,\n // Hide broken images: many markdown sources reference remote URLs that\n // 404 or are blocked. Drop the element on load failure rather than\n // leaving the browser's broken-image placeholder.\n //\n // Media URLs (mp4/mp3/etc. or legacy `iframe`-style embeds) are swapped to a\n // native `<video>` / `<audio>` element by {@link MediaPlayer} so playable\n // media in the source document renders inline.\n img: ({ src, alt }) => {\n if (!src) {\n return null;\n }\n return <Media.Player src={src} alt={alt} classNames='w-full' />;\n },\n ol: ({ children, ...props }) => (\n <ol className='ps-6 leading-tight list-decimal' {...props}>\n {children}\n </ol>\n ),\n ul: ({ children, ...props }) => (\n <ul className='ps-6 leading-tight list-disc' {...props}>\n {children}\n </ul>\n ),\n li: ({ children, ...props }) => (\n <li className='' {...props}>\n {children}\n </li>\n ),\n pre: ({ children }) => children,\n code: ({ children, className, node }) => {\n const [, language] = /language-(\\w+)/.exec(className || '') || [];\n const inline = !className && node?.position?.start.line === node?.position?.end.line;\n if (inline) {\n return <code className='rounded-xs bg-group-surface px-1 py-0.5 text-sm text-info-text'>{children}</code>;\n }\n\n return (\n <SyntaxHighlighter\n language={language}\n classNames='mt-2 mb-2 p-2 border border-separator rounded-xs text-sm dx-group-surface'\n copyButton\n PreTag='pre'\n >\n {children}\n </SyntaxHighlighter>\n );\n },\n};\n\n// Preflight already resets heading sizes and block margins, so each block only has to avoid adding\n// its own padding, font size or leading; horizontal insets and borders do not change a line's height.\nconst uniformHeading = ({ children }: PropsWithChildren) => <div className='font-medium'>{children}</div>;\n\nconst uniformComponents: ReactMarkdownOptions['components'] = {\n h1: uniformHeading,\n h2: uniformHeading,\n h3: uniformHeading,\n h4: uniformHeading,\n h5: uniformHeading,\n h6: uniformHeading,\n p: ({ children }) => <div>{children}</div>,\n blockquote: ({ children }) => <blockquote className='ps-2 border-l-2 border-accent-text'>{children}</blockquote>,\n ul: ({ children }) => <ul className='ps-5 list-disc'>{children}</ul>,\n ol: ({ children }) => <ol className='ps-5 list-decimal'>{children}</ol>,\n pre: ({ children }) => <pre className='font-mono whitespace-pre-wrap'>{children}</pre>,\n code: ({ children }) => <code className='font-mono text-info-text'>{children}</code>,\n table: ({ children }) => <table className='border-collapse'>{children}</table>,\n th: ({ children }) => <th className='p-0 pe-4 text-start font-medium'>{children}</th>,\n td: ({ children }) => <td className='p-0 pe-4'>{children}</td>,\n // A rule or an image has no line of text to align, so it gives way to its alt text or to nothing.\n hr: () => null,\n img: ({ alt }) => (alt ? <span>{alt}</span> : null),\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { type Extension } from '@codemirror/state';\nimport React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';\n\nimport { TextEditor } from '@dxos/react-ui-editor';\nimport * as Editable from '@dxos/react-ui/Editable';\nimport * as Hooks from '@dxos/react-ui/Hooks';\nimport type * as Util from '@dxos/react-ui/Util';\nimport {\n createBasicExtensions,\n createMarkdownExtensions,\n createThemeExtensions,\n decorateMarkdown,\n fullWidth,\n inlineEdit,\n} from '@dxos/ui-editor';\nimport { mx } from '@dxos/ui-theme';\n\nimport { MarkdownView, type MarkdownViewProps } from '../MarkdownView/index.ts';\n\n/**\n * Rendered markdown that becomes a markdown editor in place.\n *\n * The behaviour is `react-ui`'s `useEditable` — the same commit/revert rules the plain `Editable`\n * follows — with the two surfaces markdown needs: `MarkdownView` at rest and a CodeMirror editor\n * while editing. It lives here rather than in `react-ui` because both surfaces depend on packages\n * that depend on `react-ui`.\n *\n * The value is markdown SOURCE either way: what the reader edits is what the preview renders.\n */\nexport type MarkdownEditableProps = Util.ThemedClassName<\n Editable.UseEditableOptions & {\n /** Shown, dimmed, when the value is empty. */\n placeholder?: string;\n /** Renderers for the preview, as `MarkdownView` takes them. */\n components?: MarkdownViewProps['components'];\n /** Renders as plain markdown with no affordance to edit. */\n readonly?: boolean;\n /**\n * A field that holds paragraphs rather than a line: `Enter` stays a newline, so leaving the\n * field is what commits.\n */\n multiline?: boolean;\n /**\n * Whether opening the editor takes focus. True for click-to-edit, where the reader just asked\n * for it; false for a field held open in a pane, which would otherwise pull focus away from\n * whatever the reader is actually driving.\n */\n autoFocus?: boolean;\n /**\n * Editor extensions beyond the field's own — what a host's plugins contribute, such as link\n * chips. Appended after the markdown set, so they see a parsed document.\n */\n extensions?: Extension[];\n }\n>;\n\n/**\n * Drives a field held open in a pane, where the reader never blurs it — a Save or Cancel button\n * elsewhere on the form has to say what happens to the pending text.\n */\nexport type MarkdownEditableController = {\n /** Writes the pending text, as leaving the field would. */\n commit: () => void;\n /** Restores the last committed text, as `Escape` would. */\n revert: () => void;\n};\n\n/**\n * Rendered markdown that becomes a markdown editor in place. Pair it with `editing` held open to\n * make an editor pane, or leave it uncontrolled for click-to-edit.\n */\nexport const MarkdownEditable = forwardRef<MarkdownEditableController, MarkdownEditableProps>(\n (\n {\n classNames,\n placeholder,\n components,\n readonly,\n multiline,\n autoFocus = true,\n extensions: hostExtensions,\n ...options\n }: MarkdownEditableProps,\n forwardedRef,\n ) => {\n const { value, draft, editing, setDraft, commit, revert, previewProps } = Editable.useEditable({\n ...options,\n disabled: options.disabled || readonly,\n });\n\n // Held in a ref, and the extensions built once: `commit` closes over the draft, so it is a new\n // function on every keystroke — and a new extensions array tears the editor down and builds it\n // again, losing focus and the character just typed.\n const handlers = useRef({ setDraft, commit, revert });\n handlers.current = { setDraft, commit, revert };\n\n // The editor owns its document (see `initialValue` below), so reverting the draft alone would\n // leave the reader's text on screen — the editor is rebuilt around the restored text instead.\n const [epoch, setEpoch] = useState(0);\n\n // Tearing that editor down fires a blur, and blur commits: without this a revert would write\n // the very text it just discarded. Cleared by the next keystroke, so a field the reader returns\n // to still commits when they leave it.\n const discarded = useRef(false);\n const revertAll = useCallback(() => {\n discarded.current = true;\n handlers.current.revert();\n setEpoch((current) => current + 1);\n }, []);\n\n useImperativeHandle(forwardedRef, () => ({ commit: () => handlers.current.commit(), revert: revertAll }), [\n revertAll,\n ]);\n\n const themeMode = Hooks.useThemeMode();\n const commitOnBlur = options.blurBehavior !== 'revert';\n const extensions = useMemo(\n () => [\n // The markdown bundle is only half an editor — without the basic set the content is\n // `white-space: pre`, so a description runs off the side instead of wrapping, and there is no\n // undo. Line numbers and the active-line highlight belong to a document, not a field.\n createBasicExtensions({ lineWrapping: true, highlightActiveLine: false, placeholder, tabbable: true }),\n // Tab moves to the next field rather than indenting: this is a field in a form, and a stop\n // that swallows Tab is a trap.\n createMarkdownExtensions({ indentWithTab: false }),\n // Without the theme the caret keeps CodeMirror's own 1px black, which is invisible against a\n // dark surface. `fullWidth` rather than the default `grow`: this is a field that grows with\n // its content, not a pane filling a height.\n createThemeExtensions({ themeMode, syntaxHighlighting: true, slots: fullWidth }),\n decorateMarkdown(),\n hostExtensions ?? [],\n inlineEdit({\n // The text comes with the event: committing the draft instead would write whatever the\n // previous render captured.\n onCommit: (text) => {\n if (!discarded.current) {\n handlers.current.commit(text);\n }\n },\n onRevert: () => revertAll(),\n commitOnBlur,\n submitOnEnter: !multiline,\n }),\n ],\n [commitOnBlur, multiline, placeholder, themeMode, revertAll, hostExtensions],\n );\n\n if (editing) {\n // Wrapped rather than styled directly: `TextEditor` forwards its rest props to the editor's\n // config, not to the DOM, and the preview is a box of the same kind — so the two match.\n return (\n // CodeMirror insets its own content, which would sit the text further in than the preview it\n // replaced; the field owns its inset, so the editor's is removed. Lines keep theirs, since\n // forcing it to zero would override a list item's hanging indent and hide its bullet.\n <div data-testid='markdownEditable.editor' className={mx('w-full [&_.cm-content]:!p-0', classNames)}>\n {/* `initialValue`, not a controlled value: the editor owns its document once open, and\n feeding `draft` back in on every keystroke would fight the cursor. */}\n <TextEditor\n key={epoch}\n // The text itself is the tab stop (`tabbable` above). `TextEditor` otherwise puts a\n // tabindex on its wrapper, so Tab lands on a box that shows nothing and needs Enter to\n // get into — right for a document pane, wrong for a field next to a title.\n focusable={false}\n initialValue={draft}\n extensions={extensions}\n autoFocus={autoFocus}\n selectionEnd\n onChange={(text) => {\n discarded.current = false;\n handlers.current.setDraft(text);\n }}\n />\n </div>\n );\n }\n\n return (\n <div\n {...(readonly ? {} : previewProps)}\n data-testid='markdownEditable.preview'\n className={mx('w-full', !readonly && 'cursor-text', !value && placeholder && 'text-placeholder', classNames)}\n >\n <MarkdownView content={value || placeholder} components={components} />\n </div>\n );\n },\n);\n\nMarkdownEditable.displayName = 'MarkdownEditable';\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\nimport * as Stream from 'effect/Stream';\n\nimport { Obj } from '@dxos/echo';\n\nexport const renderObjectLink = (obj: Obj.Unknown, block?: boolean) =>\n `${block ? '!' : ''}[${Obj.getLabel(obj)}](${Obj.getURI(obj)})`;\n\nexport type StreamerOptions = {\n /**\n * How to subdivide plain-text spans before emitting them downstream.\n * - `'span'` (default) — keep entire spans intact; one CM dispatch per chunk that arrived at the source.\n * - `'word'` — split text spans at whitespace boundaries (whitespace runs become their own tokens).\n * - `'character'` — split text spans into individual characters.\n * XML/HTML fragments (`<tag>…</tag>`, self-closing tags, hyphenated custom elements) are always\n * emitted as one token regardless of `chunkSize`, otherwise widget mounting would see partial markup.\n */\n chunkSize?: 'character' | 'word' | 'span';\n /**\n * Inter-token delay in ms. Default `0`. Useful for slowing down rapid bursts so the\n * downstream renderer (CodeMirror) can show a visible streaming cadence even when the\n * source emits large chunks at once.\n */\n delayMs?: number;\n};\n\n/**\n * Streams tokens to the consumer, keeping XML/HTML fragments intact.\n *\n * The cadence is controlled by `options.chunkSize`. `'span'` (default) preserves the\n * one-token-per-source-chunk behaviour the function had originally. Use `'word'` or\n * `'character'` to decouple the visible cadence from the source's chunk size — useful when\n * the AI service emits large partial blocks but you want a smoother typewriter effect.\n */\nexport const createStreamer = (\n source: Stream.Stream<string>,\n { chunkSize = 'span', delayMs = 0 }: StreamerOptions = {},\n) => {\n const subdivide =\n chunkSize === 'span'\n ? (token: string) => [token]\n : (token: string) => (isXmlFragment(token) ? [token] : splitTextSpan(token, chunkSize));\n\n let stream: Stream.Stream<string> = source.pipe(\n Stream.flatMap((chunk) => Stream.fromIterable(splitFragments(chunk).flatMap(subdivide))),\n );\n if (delayMs > 0) {\n stream = stream.pipe(Stream.tap(() => Effect.sleep(`${delayMs} millis`)));\n }\n return stream;\n};\n\n/** A token starts with `<` if and only if it is an XML/HTML fragment produced by `splitFragments`. */\nconst isXmlFragment = (token: string): boolean => token.startsWith('<');\n\n/**\n * Subdivide a non-XML text span into smaller tokens. `'word'` splits into runs of non-whitespace\n * and runs of whitespace as separate tokens (so the renderer can display whitespace cadence too).\n */\nconst splitTextSpan = (span: string, chunkSize: 'character' | 'word'): string[] => {\n if (chunkSize === 'character') {\n return [...span];\n }\n return span.match(/\\s+|\\S+/g) ?? [span];\n};\n\n/** Matches opening tag names, including custom elements with hyphens (e.g. dom-widget). */\nconst OPENING_TAG_NAME = /^<([a-zA-Z][\\w-]*)(?:\\s[^>]*)?>/;\n\n/**\n * Splits text into chunks, preserving XML/HTML fragments.\n */\nexport const splitFragments = (text: string): string[] => {\n // First tokenize with tags to get tags as complete tokens.\n const initialTokens = splitSpans(text);\n const tokens: string[] = [];\n\n let i = 0;\n while (i < initialTokens.length) {\n const token = initialTokens[i];\n\n // Check if this is an opening tag.\n if (token.startsWith('<') && !token.startsWith('</') && !token.endsWith('/>')) {\n const tagMatch = token.match(OPENING_TAG_NAME);\n if (tagMatch) {\n const tagName = tagMatch[1];\n const closingTag = `</${tagName}>`;\n\n // Collect tokens until we find the closing tag.\n let fragment = token;\n let foundClosing = false;\n let j = i + 1;\n while (j < initialTokens.length) {\n fragment += initialTokens[j];\n if (initialTokens[j] === closingTag) {\n foundClosing = true;\n break;\n }\n j++;\n }\n\n if (foundClosing) {\n // Return the complete element as one token.\n tokens.push(fragment);\n i = j + 1;\n } else {\n // No closing tag found, just add the opening tag.\n tokens.push(token);\n i++;\n }\n } else {\n // Not a valid opening tag.\n tokens.push(token);\n i++;\n }\n } else {\n // Not an opening tag (could be closing tag, self-closing, or regular character).\n tokens.push(token);\n i++;\n }\n }\n\n return tokens;\n};\n\n// TODO(burdon): Split into paragraphs and tags.\n// export const tokenizeWithTags = (text: string): string[] => {\n// const tokens: string[] = [];\n// let i = 0;\n// while (i < text.length) {\n// if (text[i] === '<') {\n// // Find the closing bracket.\n// const closeIndex = text.indexOf('>', i);\n// if (closeIndex !== -1) {\n// // Include the complete tag.\n// tokens.push(text.slice(i, closeIndex + 1));\n// i = closeIndex + 1;\n// } else {\n// // No closing bracket found, return the entire remaining fragment.\n// tokens.push(text.slice(i));\n// break;\n// }\n// } else {\n// // Regular character.\n// tokens.push(text[i]);\n// i++;\n// }\n// }\n// return tokens;\n// };\n\nexport const splitSpans = (text: string): string[] => {\n const spans: string[] = [];\n let currentText = '';\n\n let i = 0;\n while (i < text.length) {\n if (text[i] === '<') {\n // If we have accumulated text, split it into sentences and push them.\n if (currentText) {\n // const sentences = splitSentences(currentText);\n // spans.push(...sentences);\n spans.push(currentText);\n currentText = '';\n }\n\n // Find the closing bracket.\n const closeIndex = text.indexOf('>', i);\n if (closeIndex !== -1) {\n // Include the complete tag.\n spans.push(text.slice(i, closeIndex + 1));\n i = closeIndex + 1;\n } else {\n // No closing bracket found, treat the rest as text.\n currentText = text.slice(i);\n break;\n }\n } else {\n // Accumulate regular text.\n currentText += text[i];\n i++;\n }\n }\n\n // Push any remaining text split into sentences.\n if (currentText) {\n spans.push(currentText);\n // const sentences = splitSentences(currentText);\n // spans.push(...sentences);\n }\n\n return spans;\n};\n\n/**\n * Split text into sentences, preserving the sentence-ending punctuation.\n */\nexport const splitSentences = (text: string): string[] => {\n // Match sentences ending with ., !, or ? followed by space or end of string.\n // This regex captures the sentence including its ending punctuation.\n const sentenceRegex = /[^.!?]*[.!?]+(?:\\s+|$)/g;\n const sentences: string[] = [];\n let lastIndex = 0;\n let match;\n\n while ((match = sentenceRegex.exec(text)) !== null) {\n sentences.push(match[0]);\n lastIndex = match.index + match[0].length;\n }\n\n // If there's remaining text that doesn't end with punctuation, include it.\n if (lastIndex < text.length) {\n const remaining = text.slice(lastIndex);\n if (remaining) {\n sentences.push(remaining);\n }\n }\n\n // If no sentences were found, return the original text.\n if (sentences.length === 0 && text) {\n sentences.push(text);\n }\n\n return sentences;\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\n/**\n * Options for the streaming text generator.\n */\nexport type TextStreamOptions = {\n /** Delay between chunks in ms. */\n chunkDelay?: number;\n /** Variance in timing (0-1). */\n variance?: number;\n /** Number of words per chunk. */\n wordsPerChunk?: number;\n};\n\n/**\n * Simulates word-by-word streaming (more natural for LLMs).\n */\nexport async function* textStream(\n text: string,\n options: TextStreamOptions = {},\n): AsyncGenerator<string, void, unknown> {\n const { chunkDelay = 100, variance = 0.3, wordsPerChunk = 3 } = options;\n\n // Split into words while preserving whitespace.\n const words = text.match(/\\S+|\\s+/g) || [];\n\n let i = 0;\n while (i < words.length) {\n // Collect multiple words for this chunk.\n const chunkWords: string[] = [];\n\n // Add words up to wordsPerChunk (counting only non-whitespace as words).\n let wordCount = 0;\n while (i < words.length && wordCount < wordsPerChunk) {\n const word = words[i];\n chunkWords.push(word);\n\n // Only count non-whitespace as words.\n if (word.trim()) {\n wordCount++;\n }\n i++;\n }\n\n // Yield the chunk.\n const chunk = chunkWords.join('');\n yield chunk;\n\n // Calculate delay based on chunk length.\n const varianceMultiplier = 1 + (Math.random() - 0.5) * variance * 2;\n const delay = chunkDelay * varianceMultiplier;\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n}\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { EditorView } from '@codemirror/view';\nimport * as Queue from 'effect/Queue';\nimport { type RefObject } from 'react';\n\nimport { addEventListener } from '@dxos/async';\nimport * as EffectEx from '@dxos/effect/EffectEx';\nimport {\n crawlerLineEffect,\n navigateNextEffect,\n navigatePreviousEffect,\n widgetContextEffect,\n widgetUpdateEffect,\n} from '@dxos/ui-editor';\n\nimport { type DocumentRange, type MarkdownStreamController } from './MarkdownStream.tsx';\n\n// Kept out of `MarkdownStream.tsx`: react-refresh only fast-refreshes a module whose\n// exports are all components, so values exported beside them force a full page reload on\n// every edit.\n\n/**\n * External controller API.\n *\n * @internal Exported for tests via `@dxos/react-ui-markdown/testing`; hosts get this through the\n * component's ref.\n */\nexport const computeVisibleRange = (view: EditorView): DocumentRange => {\n const rect = view.scrollDOM.getBoundingClientRect();\n // `posAtCoords(_, false)` clamps to the nearest position rather than returning null.\n const from = view.posAtCoords({ x: rect.left + 1, y: rect.top + 1 }, false);\n const to = view.posAtCoords({ x: rect.left + 1, y: rect.bottom - 1 }, false);\n return { from, to };\n};\n\nexport type MarkdownStreamControllerDeps = {\n contentRef: RefObject<string | undefined>;\n viewRef: RefObject<EditorView | null>;\n queueRef: RefObject<Queue.Queue<string>>;\n /**\n * Holds a context set before the view existed, for {@link flushContext} to apply once it does.\n * Also re-applied after a document reset, which rebuilds decorations from a state without it.\n */\n pendingContextRef: RefObject<{ value: any } | undefined>;\n onReset: (text: string) => Promise<void>;\n};\n\nexport const createMarkdownStreamController = ({\n contentRef,\n viewRef,\n queueRef,\n pendingContextRef,\n onReset,\n}: MarkdownStreamControllerDeps): MarkdownStreamController => {\n return {\n get length() {\n return viewRef.current?.state.doc.length;\n },\n\n /** Focus the editor. */\n focus: () => {\n viewRef.current?.focus();\n },\n\n /** Scroll to bottom. */\n scrollToBottom: (behavior?: ScrollBehavior) => {\n viewRef.current?.dispatch({\n effects: crawlerLineEffect.of({ line: -1, behavior }),\n });\n },\n\n /** Scroll the given document position into view. */\n scrollTo: (pos: number, options?: { y?: 'start' | 'center' | 'end' | 'nearest' }) => {\n const view = viewRef.current;\n if (!view) {\n return;\n }\n\n const clamped = Math.max(0, Math.min(pos, view.state.doc.length));\n view.dispatch({ effects: EditorView.scrollIntoView(clamped, { y: options?.y ?? 'start' }) });\n },\n\n /** The document range currently visible in the viewport. */\n getVisibleRange: () => {\n const view = viewRef.current;\n return view ? computeVisibleRange(view) : undefined;\n },\n\n /** Subscribe to visible-range changes (scroll). Fires immediately with the current range. */\n onVisibleRangeChange: (cb: (range: DocumentRange) => void) => {\n const view = viewRef.current;\n if (!view) {\n return () => {};\n }\n const handler = () => cb(computeVisibleRange(view));\n handler();\n return addEventListener(view.scrollDOM, 'scroll', handler, { passive: true });\n },\n\n /** Navigate previous prompt. */\n navigatePrevious: () => {\n viewRef.current?.dispatch({\n effects: navigatePreviousEffect.of(),\n });\n },\n\n /** Navigate next prompt. */\n navigateNext: () => {\n viewRef.current?.dispatch({\n effects: navigateNextEffect.of(),\n });\n },\n\n /**\n * Set the context for widgets (XML tags).\n *\n * Remembered as well as dispatched: a host has no signal for when the view exists, and the\n * dispatch is a no-op before it does — so without this a context set on mount is silently lost and\n * every widget callback through it dies on an optional call. {@link flushContext} re-applies it.\n */\n setContext: (context: any) => {\n pendingContextRef.current = { value: context };\n viewRef.current?.dispatch({\n effects: widgetContextEffect.of(context),\n });\n },\n\n /** Re-applies the last context set, if any. Called once the view exists and after a reset. */\n flushContext: () => {\n const pending = pendingContextRef.current;\n if (pending && viewRef.current) {\n viewRef.current.dispatch({\n effects: widgetContextEffect.of(pending.value),\n });\n }\n },\n\n /** Reset document. */\n setContent: onReset,\n\n /** Append to queue (and stream). */\n append: async (text: string) => {\n contentRef.current += text;\n if (text.length) {\n // Always go through the streaming queue, even when the doc starts empty. Skipping the\n // queue in that case (via `onReset`) bypasses the `typewriter` extension's transaction filter\n // and the first chunk lands in one CM dispatch — defeating the typewriter for any\n // consumer (e.g. ChatThread) where the first delta is large because upstream batching\n // collected several streaming partials before React rendered.\n const queue = queueRef.current;\n if (queue) {\n await EffectEx.runAndForwardErrors(Queue.offer(queue, text));\n }\n }\n },\n\n /** Update widget state. */\n updateWidget: (id: string, value: any) => {\n viewRef.current?.dispatch({\n effects: widgetUpdateEffect.of({ id, value }),\n });\n },\n } satisfies MarkdownStreamController;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { type Extension, StateEffect, StateField } from '@codemirror/state';\nimport { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';\n\nimport { Domino } from '@dxos/ui';\nimport { typewriterDrainingEffect } from '@dxos/ui-editor';\n\n/**\n * Host-controlled visibility intent. The footer block widget is rendered only when this is\n * `true` AND typewriter is not actively draining its typewriter buffer — the latter is observed\n * through {@link typewriterDrainingEffect}. Removing the decoration during a drip avoids the\n * scroll-measure conflict between CM's view-line model and the floating absolute child.\n */\nexport const setFooterVisibleEffect = StateEffect.define<boolean>();\n\ntype FooterState = {\n /** Host wants the footer rendered. */\n wanted: boolean;\n /** Typewriter is actively dripping into the document. */\n draining: boolean;\n decorations: DecorationSet;\n};\n\n/**\n * Renders a host-supplied React subtree as a CodeMirror block widget anchored at\n * `doc.length`. The widget lives inside the document, so it scrolls with content.\n *\n * Toggle the host-intent visibility via {@link setFooterVisibleEffect}. The widget is also\n * automatically removed while {@link typewriterDrainingEffect} reports `true` and re-mounted\n * once the typewriter's buffer drains — this prevents the block widget from interfering with CM's\n * view-line measurement during the typewriter drip.\n *\n * For pure doc edits (no visibility transition), the decoration's position is mapped\n * through the change set so insertions at the end translate the anchor without destroying\n * the widget's DOM.\n */\nexport const footer = (setRoot: (el: HTMLElement | null) => void): Extension => {\n const widget = new FooterWidget(setRoot);\n const buildSet = (length: number): DecorationSet =>\n Decoration.set([Decoration.widget({ widget, block: true, side: 1 }).range(length)]);\n\n const field = StateField.define<FooterState>({\n create: () => ({ wanted: false, draining: false, decorations: Decoration.none }),\n update: (state, tr) => {\n let { wanted, draining, decorations } = state;\n for (const effect of tr.effects) {\n if (effect.is(setFooterVisibleEffect)) {\n wanted = effect.value;\n }\n if (effect.is(typewriterDrainingEffect)) {\n draining = effect.value;\n }\n }\n\n // Also gate on the document being non-empty: there's nothing for the footer to anchor\n // below, and rendering it on a blank doc looks like detached chrome.\n const docLength = tr.state.doc.length;\n const visible = wanted && !draining && docLength > 0;\n const wasVisible = decorations.size > 0;\n if (visible !== wasVisible) {\n decorations = visible ? buildSet(docLength) : Decoration.none;\n } else if (tr.docChanged && decorations.size > 0) {\n // Position-map the existing decoration so insertions at the end translate the\n // widget anchor without destroying the DOM (`widget.eq` is identity-true).\n decorations = decorations.map(tr.changes);\n }\n\n return { wanted, draining, decorations };\n },\n provide: (f) => EditorView.decorations.from(f, (state) => state.decorations),\n });\n\n return [field];\n};\n\n/**\n * Block widget rendered at the end of the document. The DOM element is reported via\n * `setRoot` so the host React component can `createPortal` arbitrary content into it.\n */\nclass FooterWidget extends WidgetType {\n constructor(private readonly _setRoot: (el: HTMLElement | null) => void) {\n super();\n }\n\n // Singleton equality so CM keeps the same DOM element across decoration rebuilds —\n // the React subtree portaled into it is not unmounted on every doc change.\n override eq(_other: this): boolean {\n return true;\n }\n\n override ignoreEvent(): boolean {\n return true;\n }\n\n override toDOM(): HTMLElement {\n // The outer block-widget element is `position: relative` with zero flow height so it\n // does not push the document layout (autoScroll, line measurement, etc. ignore it).\n // The inner element is `position: absolute`, taking the React subtree out of flow — it\n // renders as a floating layer anchored to the doc tail without consuming space.\n const inner = Domino.of('div')\n .classNames('cm-stream-footer-content')\n .style({ position: 'absolute', left: '0', top: '0' });\n\n const el = Domino.of('div')\n .classNames('cm-stream-footer')\n .style({ position: 'relative', height: '0' })\n .append(inner);\n\n this._setRoot(inner.root);\n return el.root;\n }\n\n override destroy(): void {\n this._setRoot(null);\n }\n}\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { EditorSelection, type Extension, Transaction } from '@codemirror/state';\nimport { EditorView } from '@codemirror/view';\nimport * as Effect from 'effect/Effect';\nimport * as Fiber from 'effect/Fiber';\nimport * as Queue from 'effect/Queue';\nimport * as Stream from 'effect/Stream';\nimport React, {\n type ReactNode,\n type RefObject,\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n} from 'react';\nimport { createPortal } from 'react-dom';\n\nimport { addEventListener } from '@dxos/async';\nimport * as EffectEx from '@dxos/effect/EffectEx';\nimport { type UseTextEditor, useTextEditor } from '@dxos/react-ui-editor';\nimport * as Hooks from '@dxos/react-ui/Hooks';\nimport * as Status from '@dxos/react-ui/Status';\nimport type * as Util from '@dxos/react-ui/Util';\nimport {\n type AutoScrollProps,\n PROMPT_ELEMENT,\n ThemeExtensionsOptions,\n type WidgetState,\n type WidgetStateManager,\n type XmlTagsOptions,\n createBasicExtensions,\n createThemeExtensions,\n createTurnSource,\n decorateMarkdown,\n documentSlots,\n extendedMarkdown,\n fader,\n lineSpacing,\n objectLinks,\n scroller,\n turnFolding,\n typewriter,\n typewriterBypass,\n widgetContextEffect,\n widgetHost,\n widgetResetEffect,\n xmlBlockDecoration,\n xmlFormatting,\n xmlTags,\n} from '@dxos/ui-editor';\nimport { mx } from '@dxos/ui-theme';\nimport { isTruthy } from '@dxos/util';\n\nimport { createMarkdownStreamController } from './create-controller.ts';\nimport { footer, setFooterVisibleEffect } from './footer.ts';\nimport { type StreamerOptions, createStreamer } from './stream.ts';\n\n/** Document offset range (CodeMirror positions). */\nexport type DocumentRange = { from: number; to: number };\n\nexport interface MarkdownStreamController extends WidgetStateManager {\n get length(): number | undefined;\n focus: () => void;\n scrollToBottom: (behavior?: ScrollBehavior) => void;\n /** Scroll the given document position into view. */\n scrollTo: (pos: number, options?: { y?: 'start' | 'center' | 'end' | 'nearest' }) => void;\n /** The document range currently visible in the viewport, or `undefined` if not mounted. */\n getVisibleRange: () => DocumentRange | undefined;\n /** Subscribe to visible-range changes (fires immediately with the current range). Returns an unsubscribe fn. */\n onVisibleRangeChange: (cb: (range: DocumentRange) => void) => () => void;\n navigateNext: () => void;\n navigatePrevious: () => void;\n setContext: (context: any) => void;\n /** Re-applies the last context set. Idempotent; a no-op when none was set or the view is absent. */\n flushContext: () => void;\n setContent: (text: string) => Promise<void>;\n append: (text: string) => Promise<void>;\n}\n\n/** Map the scroll container's top/bottom edges to document positions. */\n\nexport type MarkdownStreamEvent = {\n type: 'submit';\n value: string | null;\n};\n\nexport type MarkdownStreamProps = Util.ThemedClassName<\n {\n debug?: boolean;\n\n /** Initial content. */\n content?: string;\n\n /** View options. */\n options?: {\n autoScroll?: boolean;\n typewriter?: boolean;\n cursor?: boolean;\n fader?: boolean;\n\n /**\n * Streaming cadence. See {@link StreamerOptions}.\n * Use `'word'` or `'character'` to break large source chunks into smaller CM dispatches —\n * useful when the AI service emits big partial blocks but you want a smoother typewriter\n * effect. Combine with `streamDelayMs` to add a per-token sleep.\n * Default: `'span'` (one CM dispatch per source chunk; current behaviour).\n */\n streamCadence?: StreamerOptions['chunkSize'];\n\n /**\n * Per-token delay (ms) for the streaming queue. Default `0`.\n */\n streamDelayMs?: number;\n };\n\n /**\n * Optional React subtree rendered as a CodeMirror block-widget decoration anchored at\n * `doc.length`. Scrolls with the document content (lives inside `cm-content`'s flow).\n * Visibility tracks the truthiness of the prop.\n */\n footer?: ReactNode;\n\n /**\n * Extra CodeMirror extensions appended after the built-in stack — use for keymaps or\n * other host-level behaviour (e.g. `Mod-d` to toggle debug) that should apply when the\n * document is focused.\n */\n extensions?: Extension;\n\n /**\n * Theme extensions.\n */\n slots?: ThemeExtensionsOptions['slots'];\n\n /** Event handler. */\n onEvent?: (event: MarkdownStreamEvent) => void;\n } & (XmlTagsOptions & AutoScrollProps)\n>;\n\n/**\n * Codemirror-based markdown editor with xml tag widtgets and streaming support.\n */\nexport const MarkdownStream = forwardRef<MarkdownStreamController | null, MarkdownStreamProps>(\n ({ classNames, debug, content, options, registry, extensions, footer, slots, onEvent }, forwardedRef) => {\n // Store current content so that we can toggle debug mode. Default to '' so the\n // `append()` path (which does `contentRef.current += text`) doesn't concatenate\n // against `undefined` and stamp `\"undefined\"` into the transcript snapshot.\n const contentRef = useRef(content ?? '');\n\n // DOM node for the footer block widget — populated when its decoration mounts.\n const [footerRoot, setFooterRoot] = useState<HTMLElement | null>(null);\n\n // Survives a context set before the view exists; flushed once it does (see `flushContext`).\n const pendingContextRef = useRef<{ value: any } | undefined>(undefined);\n\n // Codemirror editor.\n const { parentRef, view, viewRef, widgets } = useMarkdownStreamTextEditor(contentRef, {\n slots,\n debug,\n registry,\n options,\n extensions,\n setFooterRoot: footer ? setFooterRoot : undefined,\n });\n\n // Show the status footer.\n const footerVisible = !!footer;\n useEffect(() => {\n view?.dispatch({ effects: setFooterVisibleEffect.of(footerVisible) });\n }, [view, footerVisible]);\n\n // Streaming text queue.\n const [queue, setQueue, queueRef] = Hooks.useStateWithRef(Effect.runSync(Queue.unbounded<string>()));\n\n // Reset document.\n const onReset = useCallback(\n async (text: string) => {\n contentRef.current = text;\n if (!viewRef.current) {\n return;\n }\n\n // Set content and scroll to bottom. Re-assert the host context rather than clearing it: it\n // belongs to the host, not the document, so nulling it here would strand every widget in the\n // replacement document with `context: undefined`.\n viewRef.current.dispatch({\n effects: [widgetContextEffect.of(pendingContextRef.current?.value ?? null), widgetResetEffect.of(null)],\n changes: [{ from: 0, to: viewRef.current.state.doc.length, insert: text }],\n annotations: typewriterBypass.of(true),\n selection: EditorSelection.cursor(text.length),\n });\n\n // New queue.\n setQueue(Effect.runSync(Queue.unbounded<string>()));\n },\n [contentRef, viewRef, setQueue, pendingContextRef],\n );\n\n // Controller API.\n useImperativeHandle(\n forwardedRef,\n () => createMarkdownStreamController({ contentRef, viewRef, queueRef, onReset, pendingContextRef }),\n [onReset],\n );\n\n // Apply a context that was set before the view existed — the dispatch in `setContext` is a no-op\n // until then, and a host mounting the controller has no way to know.\n useEffect(() => {\n const pending = pendingContextRef.current;\n if (view && pending) {\n view.dispatch({ effects: widgetContextEffect.of(pending.value) });\n }\n }, [view]);\n\n // Widget events.\n useEffect(() => {\n if (!parentRef.current) {\n return;\n }\n\n // TODO(burdon): Replace this hack with a custom event listener from widgets.\n return addEventListener(parentRef.current, 'click', (event) => {\n const button = (event.target as HTMLElement).closest('[data-action=\"submit\"]');\n if (button?.getAttribute('data-action') === 'submit') {\n onEvent?.({\n type: 'submit',\n value: button.getAttribute('data-value'),\n });\n }\n });\n }, [view, parentRef, onEvent]);\n\n // Consume queue and update document.\n useMarkdownStreamQueue(view, queue, {\n chunkSize: options?.streamCadence,\n delayMs: options?.streamDelayMs,\n });\n\n // Cleanup.\n useEffect(() => {\n return () => {\n view?.destroy();\n };\n }, [view]);\n\n return (\n <>\n {/* Markdown editor. */}\n <div className={mx('dx-expand', classNames)} ref={parentRef} />\n\n {/* React widgets are rendered in portals outside of the editor. */}\n <Status.ErrorBoundary name='markdown-stream'>\n {widgets.map(({ Component, root, id, props }) => (\n <div key={id}>{createPortal(<Component view={view} {...props} />, root)}</div>\n ))}\n {footerRoot && footerVisible && createPortal(footer, footerRoot)}\n </Status.ErrorBoundary>\n </>\n );\n },\n);\n\n// Fold each agent response beneath its `<prompt>` (the head element rendered by `xmlBlockDecoration`).\nconst turnSource = createTurnSource(PROMPT_ELEMENT);\n\ntype MarkdownStreamTextEditorParams = Pick<MarkdownStreamProps, 'debug' | 'registry' | 'options' | 'extensions'> &\n Pick<ThemeExtensionsOptions, 'slots'> & {\n setFooterRoot?: (el: HTMLElement | null) => void;\n };\n\ntype MarkdownStreamTextEditorResult = UseTextEditor & {\n viewRef: RefObject<EditorView | null>;\n widgets: WidgetState[];\n};\n\n/**\n * Read-only markdown editor configured for streaming (theme, markdown extensions, XML widgets).\n */\nconst useMarkdownStreamTextEditor = (\n currentContent: RefObject<string | undefined>,\n {\n debug,\n registry,\n options,\n extensions: extensionsProp,\n slots = documentSlots,\n setFooterRoot,\n }: MarkdownStreamTextEditorParams,\n): MarkdownStreamTextEditorResult => {\n const themeMode = Hooks.useThemeMode();\n\n // Active widgets.\n const [widgets, setWidgets] = useState<WidgetState[]>([]);\n\n // Editor.\n const { view, parentRef } = useTextEditor(() => {\n const content = currentContent.current;\n return {\n initialValue: content,\n selection: EditorSelection.cursor(content?.length ?? 0),\n extensions: [\n createBasicExtensions({ lineWrapping: true, readOnly: true }),\n createThemeExtensions({ slots, scrollbarThin: true, syntaxHighlighting: true, themeMode }),\n xmlFormatting({ skip: debug ? [] : ['prompt'] }),\n !debug &&\n [\n extendedMarkdown({ registry }),\n decorateMarkdown(),\n lineSpacing(),\n xmlBlockDecoration({\n tag: 'prompt',\n lineClass:\n 'cm-prompt-line cm-prompt-bubble bg-group-surface text-fg border-l-[8px] pl-[8px]! pr-2 [&_*]:text-inherit!',\n firstLineClass: 'pt-1.5 rounded-t-sm',\n lastLineClass: 'pb-1.5 rounded-b-sm',\n hideTags: true,\n }),\n widgetHost({ setWidgets, bookmarks: ['prompt'] }),\n xmlTags({ registry }),\n objectLinks(),\n turnFolding({ source: turnSource }),\n scroller({ overScroll: 80, autoScroll: options?.autoScroll }),\n options?.typewriter &&\n typewriter({\n cursor: options?.cursor,\n streamingTags: new Set(\n Object.entries(registry ?? {})\n .filter(([, def]) => def.streaming)\n .map(([tag]) => tag),\n ),\n }),\n options?.fader && fader(),\n setFooterRoot && footer(setFooterRoot),\n ].filter(isTruthy),\n extensionsProp,\n ].filter(isTruthy),\n };\n }, [\n themeMode,\n registry,\n debug,\n options?.autoScroll,\n options?.typewriter,\n options?.cursor,\n options?.fader,\n slots,\n extensionsProp,\n ]);\n\n const viewRef = Hooks.useDynamicRef(view);\n return { view, viewRef, parentRef, widgets };\n};\n\n/**\n * Consumes streaming text from the queue and appends it to the editor document.\n */\nconst useMarkdownStreamQueue = (\n view: EditorView | null,\n queue: Queue.Queue<string>,\n streamerOptions?: StreamerOptions,\n) => {\n const chunkSize = streamerOptions?.chunkSize;\n const delayMs = streamerOptions?.delayMs;\n useEffect(() => {\n if (!view) {\n return;\n }\n\n // Consume queue and update document.\n const fork = Stream.fromQueue(queue).pipe(\n (source) => createStreamer(source, { chunkSize, delayMs }),\n Stream.runForEach((text) =>\n Effect.sync(() => {\n const scrollTop = view.scrollDOM.scrollTop;\n view.dispatch({\n changes: [{ from: view.state.doc.length, insert: text }],\n annotations: Transaction.remote.of(true),\n scrollIntoView: false,\n });\n\n // Prevent autoscrolling.\n requestAnimationFrame(() => {\n view.scrollDOM.scrollTop = scrollTop;\n });\n }),\n ),\n Effect.runFork,\n );\n\n return () => {\n void EffectEx.runAndForwardErrors(Fiber.interrupt(fork));\n };\n }, [view, queue, chunkSize, delayMs]);\n};\n"],"mappings":"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"}
|