@cardstack/boxel-cli 0.11.0-unstable.2 → 0.11.0-unstable.3
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/bundled-realms/pretui/components/agent-mark.gts +98 -0
- package/bundled-realms/pretui/components/agent-question.gts +340 -37
- package/bundled-realms/pretui/components/answer-footer.gts +249 -0
- package/bundled-realms/pretui/components/card-embed.gts +523 -0
- package/bundled-realms/pretui/components/card-pills.gts +725 -0
- package/bundled-realms/pretui/components/code-block.gts +87 -45
- package/bundled-realms/pretui/components/collapsed-turn.gts +133 -12
- package/bundled-realms/pretui/components/compaction-chip.gts +81 -67
- package/bundled-realms/pretui/components/composer-cap.gts +516 -0
- package/bundled-realms/pretui/components/context-ring.gts +108 -0
- package/bundled-realms/pretui/components/diff-block.gts +1002 -31
- package/bundled-realms/pretui/components/diff-stat.gts +102 -0
- package/bundled-realms/pretui/components/jump-chip.gts +86 -53
- package/bundled-realms/pretui/components/markdown-stream.gts +799 -0
- package/bundled-realms/pretui/components/markdown-surface.gts +71 -0
- package/bundled-realms/pretui/components/plan-mark.gts +147 -0
- package/bundled-realms/pretui/components/prose.gts +185 -8
- package/bundled-realms/pretui/components/prose.md +6 -8
- package/bundled-realms/pretui/components/quick-look.gts +377 -0
- package/bundled-realms/pretui/components/reactions.gts +136 -0
- package/bundled-realms/pretui/components/recommendation.gts +67 -36
- package/bundled-realms/pretui/components/result-card.gts +58 -22
- package/bundled-realms/pretui/components/segmented-control.gts +13 -8
- package/bundled-realms/pretui/components/session-prep.gts +12 -11
- package/bundled-realms/pretui/components/sliding-highlight.gts +8 -3
- package/bundled-realms/pretui/components/speaker-label.gts +132 -0
- package/bundled-realms/pretui/components/state-glyph.gts +121 -0
- package/bundled-realms/pretui/components/step-list.gts +62 -60
- package/bundled-realms/pretui/components/stick-to-bottom.gts +1150 -0
- package/bundled-realms/pretui/components/system-line.gts +143 -0
- package/bundled-realms/pretui/components/task-checklist.gts +184 -0
- package/bundled-realms/pretui/components/thinking.gts +63 -94
- package/bundled-realms/pretui/components/tool-change-table.gts +285 -0
- package/bundled-realms/pretui/components/tool-gate.gts +462 -0
- package/bundled-realms/pretui/components/tool-group.gts +920 -0
- package/bundled-realms/pretui/components/tool-icon.ts +64 -0
- package/bundled-realms/pretui/components/tool-meta.gts +82 -0
- package/bundled-realms/pretui/components/tool-plan.gts +197 -0
- package/bundled-realms/pretui/components/tool-row.gts +2087 -0
- package/bundled-realms/pretui/components/turn-changes.gts +227 -0
- package/bundled-realms/pretui/components/user-bubble.gts +575 -0
- package/bundled-realms/pretui/components/work-item.gts +185 -48
- package/bundled-realms/pretui/docs/scroll-anchoring-protocol.md +136 -0
- package/bundled-realms/pretui/internal/markdown-projection.ts +438 -0
- package/bundled-realms/pretui/internal/mount-queue.ts +79 -0
- package/bundled-realms/pretui/internal/tool-phrases.ts +168 -0
- package/bundled-realms/pretui/package.json +2 -1
- package/bundled-test-harness/assets/{ai-module-creation-test-beRNwH-e.js → ai-module-creation-test-B6FQKqIl.js} +1 -1
- package/bundled-test-harness/assets/{app-CtezUCip.js → app-KpPqdyX3.js} +4 -4
- package/bundled-test-harness/assets/{binary-upload-dedupe-test-bI8T0QLl.js → binary-upload-dedupe-test-DN00y1tM.js} +1 -1
- package/bundled-test-harness/assets/{can-invoke-test-DVaU_yRT.js → can-invoke-test-Bl-EUFM3.js} +1 -1
- package/bundled-test-harness/assets/{card-chooser-paging-test-DnxMnXKR.js → card-chooser-paging-test-BNuZRHX3.js} +1 -1
- package/bundled-test-harness/assets/{card-chooser-test-DUSnLr-u.js → card-chooser-test-wKfPOOT4.js} +1 -1
- package/bundled-test-harness/assets/{card-def-field-def-relationships-test-GBUr3tFK.js → card-def-field-def-relationships-test-CduQ6zan.js} +1 -1
- package/bundled-test-harness/assets/{card-delete-test-CQveO1VC.js → card-delete-test-DKxolj5o.js} +1 -1
- package/bundled-test-harness/assets/{check-correctness-test-D2M2p66N.js → check-correctness-test-B-kgHztp.js} +1 -1
- package/bundled-test-harness/assets/{codeblocks-test-AdolhdCA.js → codeblocks-test-BDD7qK_V.js} +1 -1
- package/bundled-test-harness/assets/{compaction-test-DvyAQq56.js → compaction-test-BweolkhG.js} +1 -1
- package/bundled-test-harness/assets/{container-DIF_9lZ9.css → container-B5B6u-xm.css} +29 -22
- package/bundled-test-harness/assets/{container-DvvqmNpg.js → container-BnJjIM6J.js} +9 -6
- package/bundled-test-harness/assets/{create-listing-modal-test-D95JfQab.js → create-listing-modal-test-FvcAKpIa.js} +1 -1
- package/bundled-test-harness/assets/{debug-message-test-BcNtj7Wr.js → debug-message-test-CJ18Evt7.js} +1 -1
- package/bundled-test-harness/assets/{externals-DCnB4XSS.js → externals-BWjhmCD4.js} +1 -1
- package/bundled-test-harness/assets/{fallback-banner-live-delete-test-5lnN1pv2.js → fallback-banner-live-delete-test-C2H3BrT-.js} +1 -1
- package/bundled-test-harness/assets/{fallback-banner-test-DFb_m_Kq.js → fallback-banner-test-C10aYjkj.js} +1 -1
- package/bundled-test-harness/assets/{fallback-models-test-p3YIwj1L.js → fallback-models-test-BxoPiaGF.js} +1 -1
- package/bundled-test-harness/assets/{file-attachment-test-CmyjNWRR.js → file-attachment-test-DeKyzYaz.js} +1 -1
- package/bundled-test-harness/assets/{general-test-CX-TFDuX.js → general-test-BewzrpoB.js} +1 -1
- package/bundled-test-harness/assets/{index-query-engine-test-CY2tAkot.js → index-query-engine-test-BtI8Y2LR.js} +1 -1
- package/bundled-test-harness/assets/{instance-filter-matcher-test-CMBTh8xn.js → instance-filter-matcher-test-BHXHGHjE.js} +1 -1
- package/bundled-test-harness/assets/{main-BOZbZVem.js → main-CqAHZ2kK.js} +1 -1
- package/bundled-test-harness/assets/{message-tool-test-CfjRSJvt.js → message-tool-test-NMaPJFsd.js} +1 -1
- package/bundled-test-harness/assets/{model-cost-tier-test-mZG84Uso.js → model-cost-tier-test-BgbfN2LH.js} +1 -1
- package/bundled-test-harness/assets/{operation-lowering-test-CHnjKSJb.js → operation-lowering-test-C4IaOjsA.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-card-chooser-test-KWS8lq16.js → operator-mode-card-chooser-test-Bwe47Rn7.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-links-test-CViFt0Ws.js → operator-mode-links-test-BU4jkC-B.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-test-D0gZM2yw.js → operator-mode-test-Bh0rGM9z.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-ui-test-CEvHJ9DS.js → operator-mode-ui-test-whgUw0ju.js} +1 -1
- package/bundled-test-harness/assets/{overlay-menu-items-test-Pw80RXsr.js → overlay-menu-items-test-Cwn1vr8q.js} +1 -1
- package/bundled-test-harness/assets/{past-sessions-test-CW0-Bjav.js → past-sessions-test-Bh24N-Kz.js} +1 -1
- package/bundled-test-harness/assets/{permission-checking-realm-test-D5uXQypS.js → permission-checking-realm-test-DKnnAy0f.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-policy-standing-test-Btatheni.js → realm-config-policy-standing-test-DngdYypq.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-routing-rule-edit-test-Dh5zW13o.js → realm-config-routing-rule-edit-test-_rkFU8SN.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-settings-test-BJa8gv_C.js → realm-config-settings-test-CR9NH0Zn.js} +1 -1
- package/bundled-test-harness/assets/{reasoning-test-2Zp37cQB.js → reasoning-test-0lH10kRl.js} +1 -1
- package/bundled-test-harness/assets/{room-message-test-BsrON13e.js → room-message-test-C-PddE4Y.js} +1 -1
- package/bundled-test-harness/assets/{run-realm-code-BxktQf6R.js → run-realm-code-C8N7DtN-.js} +1 -1
- package/bundled-test-harness/assets/{run-realm-code-test-7grpTos-.js → run-realm-code-test-DBdp147q.js} +1 -1
- package/bundled-test-harness/assets/{schema-generation-test-CM9ggQji.js → schema-generation-test-CB1Hb7GQ.js} +1 -1
- package/bundled-test-harness/assets/{scrolling-test-BER00htB.js → scrolling-test-3PhVin8h.js} +1 -1
- package/bundled-test-harness/assets/{searchable-option-test-Ctf8r3HZ.js → searchable-option-test-BAdOiL4U.js} +1 -1
- package/bundled-test-harness/assets/{sending-test-BJGobs2E.js → sending-test-KSmu1tSJ.js} +1 -1
- package/bundled-test-harness/assets/{skills-test-GPOsHRN6.js → skills-test-D-dlcFpd.js} +1 -1
- package/bundled-test-harness/assets/{store-test-QvvHimzH.js → store-test-CEW7xQjZ.js} +1 -1
- package/bundled-test-harness/assets/{tests-DoTEpvcF.js → tests-QYqqa6nv.js} +47 -47
- package/bundled-test-harness/assets/{text-input-validator-test-CYYH2N0L.js → text-input-validator-test-D8iNOWep.js} +1 -1
- package/bundled-test-harness/assets/{to-device-streaming-test-Ctqa0bVp.js → to-device-streaming-test-trCX-505.js} +1 -1
- package/bundled-test-harness/assets/{token-usage-test-BQVbK3X0.js → token-usage-test-C6sULooH.js} +1 -1
- package/bundled-test-harness/assets/{tools-DU8Rc15s.js → tools-UJfD-_al.js} +1 -1
- package/bundled-test-harness/assets/{tools-test-B13yvEKE.js → tools-test-Brav31AV.js} +1 -1
- package/bundled-test-harness/assets/{worker-DHBEBihX.js → worker-9ph9_Lpv.js} +31 -26
- package/bundled-test-harness/index.html +8 -8
- package/bundled-test-harness/tests/index.html +8 -8
- package/bundled-types/host-app/lib/realm-runner/worker.ts +8 -51
- package/bundled-types/runtime-common/realm-runner-script.d.ts +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
// Pretui — MarkdownStream: assistant prose that arrives token by token, rendered so each update costs the tail, not the whole message. Finished blocks freeze and keep their DOM; only the last block re-renders; an open code fence grows by appending.
|
|
2
|
+
import Component from '@glimmer/component';
|
|
3
|
+
import { tracked } from '@glimmer/tracking';
|
|
4
|
+
import { htmlSafe } from '@ember/template';
|
|
5
|
+
import type { SafeString } from '@ember/template';
|
|
6
|
+
import { modifier } from 'ember-modifier';
|
|
7
|
+
import { CodeBlock } from './code-block';
|
|
8
|
+
import { MarkdownSurface } from './markdown-surface';
|
|
9
|
+
import {
|
|
10
|
+
collectDefinitions,
|
|
11
|
+
heal,
|
|
12
|
+
healBfm,
|
|
13
|
+
isExternalHref,
|
|
14
|
+
isPathLike,
|
|
15
|
+
project,
|
|
16
|
+
renderPlain,
|
|
17
|
+
} from '../internal/markdown-projection';
|
|
18
|
+
import type {
|
|
19
|
+
BlockMode,
|
|
20
|
+
Directive,
|
|
21
|
+
MarkdownFlavor,
|
|
22
|
+
ProjectedBlock,
|
|
23
|
+
Projection,
|
|
24
|
+
} from '../internal/markdown-projection';
|
|
25
|
+
|
|
26
|
+
export type {
|
|
27
|
+
Directive,
|
|
28
|
+
MarkdownFlavor,
|
|
29
|
+
} from '../internal/markdown-projection';
|
|
30
|
+
|
|
31
|
+
/** What `@render` is asked to turn into HTML: one block's markdown. */
|
|
32
|
+
export interface MarkdownSource {
|
|
33
|
+
/** stable across updates */
|
|
34
|
+
key: string;
|
|
35
|
+
/** the block's markdown, healed when it is the live tail */
|
|
36
|
+
src: string;
|
|
37
|
+
flavor: MarkdownFlavor;
|
|
38
|
+
/** every link-reference and footnote definition in the whole document,
|
|
39
|
+
* one per line, for a renderer to append: a block parsed on its own cannot
|
|
40
|
+
* otherwise see a definition that lives in another block */
|
|
41
|
+
definitions: string;
|
|
42
|
+
mode: BlockMode;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A link `@resolveLink` confirmed. Anything else the caller needs to act
|
|
46
|
+
* on it (a card id, a line range) rides in `target`. */
|
|
47
|
+
export interface ResolvedLink {
|
|
48
|
+
href: string;
|
|
49
|
+
kind: 'url' | 'path';
|
|
50
|
+
target?: unknown;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface MarkdownCode {
|
|
54
|
+
key: string;
|
|
55
|
+
/** the block's position among the message's code blocks, from 0 */
|
|
56
|
+
index: number;
|
|
57
|
+
source: string;
|
|
58
|
+
language?: string;
|
|
59
|
+
/** whether the closing fence has arrived */
|
|
60
|
+
complete: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface MarkdownDirective extends Directive {
|
|
64
|
+
key: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A BFM card or file reference, inline (`:card[url]`) or block
|
|
68
|
+
* (`::card[url | size]`). */
|
|
69
|
+
export interface MarkdownRef {
|
|
70
|
+
/** stable while the reference's block is unchanged */
|
|
71
|
+
key: string;
|
|
72
|
+
type: 'card' | 'file';
|
|
73
|
+
url: string;
|
|
74
|
+
inline: boolean;
|
|
75
|
+
/** block refs: the raw `|` arguments (format or size), for the host's own
|
|
76
|
+
* parser. Inline refs: empty */
|
|
77
|
+
args: string[];
|
|
78
|
+
/** from the renderer's placeholder attributes, when it emitted them */
|
|
79
|
+
format?: string;
|
|
80
|
+
width?: string;
|
|
81
|
+
height?: string;
|
|
82
|
+
/** false while a block ref's closing `]` has not arrived: show a
|
|
83
|
+
* placeholder, not a card */
|
|
84
|
+
complete: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface MarkdownStreamSignature {
|
|
88
|
+
Args: {
|
|
89
|
+
/** the full accumulated text so far */
|
|
90
|
+
text: string;
|
|
91
|
+
/** true while tokens are still arriving */
|
|
92
|
+
streaming?: boolean;
|
|
93
|
+
/** block markdown → SANITIZED HTML, synchronously or as a promise. The
|
|
94
|
+
* component inserts what this returns; sanitizing is the renderer's job.
|
|
95
|
+
* Default: the text escaped into paragraphs. */
|
|
96
|
+
render?: (source: MarkdownSource) => string | Promise<string>;
|
|
97
|
+
/** repair the live tail's unclosed inline markup before rendering it.
|
|
98
|
+
* Default true */
|
|
99
|
+
heal?: boolean;
|
|
100
|
+
/** a relative link or path-like inline code becomes a link only when
|
|
101
|
+
* this confirms it. External http(s) links need no lookup. */
|
|
102
|
+
resolveLink?: (
|
|
103
|
+
href: string,
|
|
104
|
+
kind: 'url' | 'path',
|
|
105
|
+
) => Promise<ResolvedLink | null | undefined>;
|
|
106
|
+
/** a confirmed link was activated */
|
|
107
|
+
onLinkActivate?: (
|
|
108
|
+
link: ResolvedLink,
|
|
109
|
+
event: MouseEvent | KeyboardEvent,
|
|
110
|
+
) => void;
|
|
111
|
+
/** read politely once the stream completes. Silent by default */
|
|
112
|
+
announceComplete?: string;
|
|
113
|
+
/** 'markdown' (default) or 'bfm', Boxel Flavored Markdown: display math
|
|
114
|
+
* stays one block and never renders half-written, partial card and file
|
|
115
|
+
* references show as pending, an unclosed `$` stays literal, references
|
|
116
|
+
* render through <:ref>, and finished Mermaid fences through <:diagram> */
|
|
117
|
+
flavor?: MarkdownFlavor;
|
|
118
|
+
/** bfm: fence languages that are diagrams. Default ['mermaid'] */
|
|
119
|
+
diagramLanguages?: string[];
|
|
120
|
+
};
|
|
121
|
+
Blocks: {
|
|
122
|
+
code: [MarkdownCode];
|
|
123
|
+
directive: [MarkdownDirective];
|
|
124
|
+
ref: [MarkdownRef];
|
|
125
|
+
diagram: [MarkdownCode];
|
|
126
|
+
};
|
|
127
|
+
Element: HTMLDivElement;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface RenderedEntry {
|
|
131
|
+
/** the src the HTML on screen was rendered from */
|
|
132
|
+
src: string;
|
|
133
|
+
html: string;
|
|
134
|
+
/** the request number the HTML on screen came from: a result from an
|
|
135
|
+
* older request than this one is dropped */
|
|
136
|
+
seq: number;
|
|
137
|
+
/** reused while the HTML is unchanged, so Glimmer leaves the DOM (and any
|
|
138
|
+
* selection in it) alone and the link scan does not re-run */
|
|
139
|
+
safe?: SafeString;
|
|
140
|
+
/** the src the newest pending promise was asked for, so the same src is
|
|
141
|
+
* not requested twice */
|
|
142
|
+
requested?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface ViewBlock {
|
|
146
|
+
key: string;
|
|
147
|
+
kind: ProjectedBlock['kind'];
|
|
148
|
+
mode: BlockMode;
|
|
149
|
+
html?: SafeString;
|
|
150
|
+
code?: MarkdownCode;
|
|
151
|
+
/** bfm: a finished diagram fence */
|
|
152
|
+
diagram?: boolean;
|
|
153
|
+
directive?: MarkdownDirective;
|
|
154
|
+
/** bfm: a block card or file reference */
|
|
155
|
+
ref?: MarkdownRef;
|
|
156
|
+
/** bfm: display math still open; shown as its source, never rendered */
|
|
157
|
+
pendingMath?: string;
|
|
158
|
+
/** the streaming tail: the caret sits at the end of its text */
|
|
159
|
+
caret?: boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
interface RefSlot {
|
|
163
|
+
id: string;
|
|
164
|
+
blockKey: string;
|
|
165
|
+
element: Element;
|
|
166
|
+
ref: MarkdownRef;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const REF_SELECTOR = '[data-boxel-bfm-inline-ref], [data-boxel-bfm-block-ref]';
|
|
170
|
+
|
|
171
|
+
function refFromPlaceholder(
|
|
172
|
+
element: Element,
|
|
173
|
+
key: string,
|
|
174
|
+
): MarkdownRef | undefined {
|
|
175
|
+
let inlineUrl = element.getAttribute('data-boxel-bfm-inline-ref');
|
|
176
|
+
let url = inlineUrl ?? element.getAttribute('data-boxel-bfm-block-ref');
|
|
177
|
+
let type = element.getAttribute('data-boxel-bfm-type');
|
|
178
|
+
if (!url || (type !== 'card' && type !== 'file')) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
key,
|
|
183
|
+
type,
|
|
184
|
+
url,
|
|
185
|
+
inline: inlineUrl !== null,
|
|
186
|
+
args: [],
|
|
187
|
+
format: element.getAttribute('data-boxel-bfm-format') ?? undefined,
|
|
188
|
+
width: element.getAttribute('data-boxel-bfm-width') ?? undefined,
|
|
189
|
+
height: element.getAttribute('data-boxel-bfm-height') ?? undefined,
|
|
190
|
+
complete: true,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const LINK_STATE = 'data-markdown-link';
|
|
195
|
+
const resolvedLinks = new WeakMap<Element, ResolvedLink>();
|
|
196
|
+
|
|
197
|
+
/** Runs `linkify` on insert and whenever the block's HTML changes. Keyed on
|
|
198
|
+
* the HTML so an unchanged block is not re-scanned. */
|
|
199
|
+
const linkified = modifier(
|
|
200
|
+
(
|
|
201
|
+
element: HTMLElement,
|
|
202
|
+
[linkify, _html, key]: [
|
|
203
|
+
(el: HTMLElement, key: string) => void,
|
|
204
|
+
SafeString | undefined,
|
|
205
|
+
string,
|
|
206
|
+
],
|
|
207
|
+
) => {
|
|
208
|
+
linkify(element, key);
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
/** Both truthy: a template helper, since Glimmer has no `and`. */
|
|
213
|
+
const bothTrue = (a: unknown, b: unknown): boolean => Boolean(a) && Boolean(b);
|
|
214
|
+
|
|
215
|
+
// Node-type numbers rather than Node.TEXT_NODE: inside a realm module the
|
|
216
|
+
// name `Node` is not the browser's DOM Node, and its constants are absent.
|
|
217
|
+
const TEXT_NODE = 3;
|
|
218
|
+
const ELEMENT_NODE = 1;
|
|
219
|
+
|
|
220
|
+
function lastTextNode(node: Node): Text | null {
|
|
221
|
+
for (let i = node.childNodes.length - 1; i >= 0; i--) {
|
|
222
|
+
let child = node.childNodes[i]!;
|
|
223
|
+
if (child.nodeType === TEXT_NODE) {
|
|
224
|
+
if ((child.textContent ?? '').trim() !== '') {
|
|
225
|
+
return child as Text;
|
|
226
|
+
}
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (child.nodeType === ELEMENT_NODE) {
|
|
230
|
+
let found = lastTextNode(child);
|
|
231
|
+
if (found) {
|
|
232
|
+
return found;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Puts the streaming caret right after the last word of the live block,
|
|
240
|
+
* inside whatever element holds it (a paragraph, a list item, a cell), so it
|
|
241
|
+
* reads as the end of the sentence rather than a line of its own. Re-placed
|
|
242
|
+
* whenever the block's HTML changes; removed when the block freezes. */
|
|
243
|
+
const caretAtEnd = modifier(
|
|
244
|
+
(
|
|
245
|
+
element: HTMLElement,
|
|
246
|
+
[_html, show]: [SafeString | undefined, boolean | undefined],
|
|
247
|
+
) => {
|
|
248
|
+
element.querySelector('[data-markdown-caret]')?.remove();
|
|
249
|
+
if (!show) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
let caret = document.createElement('span');
|
|
253
|
+
caret.className = 'pretui-md-caret';
|
|
254
|
+
caret.setAttribute('aria-hidden', 'true');
|
|
255
|
+
caret.setAttribute('data-markdown-caret', '');
|
|
256
|
+
caret.setAttribute('data-test-pretui-markdown-caret', '');
|
|
257
|
+
// The block's HTML can land after this modifier installs, and an async
|
|
258
|
+
// renderer swaps it later still, so the caret follows the content:
|
|
259
|
+
// placed now, and re-placed whenever the block's children change.
|
|
260
|
+
// Placing is idempotent, so its own move settles the observer.
|
|
261
|
+
let place = () => {
|
|
262
|
+
let text = lastTextNode(element);
|
|
263
|
+
if (text) {
|
|
264
|
+
if (text.nextSibling !== caret) {
|
|
265
|
+
text.after(caret);
|
|
266
|
+
}
|
|
267
|
+
} else if (caret.parentNode !== element || element.lastChild !== caret) {
|
|
268
|
+
element.appendChild(caret);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
place();
|
|
272
|
+
let observer = new MutationObserver(place);
|
|
273
|
+
observer.observe(element, { childList: true, subtree: true });
|
|
274
|
+
return () => {
|
|
275
|
+
observer.disconnect();
|
|
276
|
+
caret.remove();
|
|
277
|
+
};
|
|
278
|
+
},
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const listen = modifier(
|
|
282
|
+
(
|
|
283
|
+
element: HTMLElement,
|
|
284
|
+
[onClick, onKeyDown]: [
|
|
285
|
+
(event: MouseEvent) => void,
|
|
286
|
+
(event: KeyboardEvent) => void,
|
|
287
|
+
],
|
|
288
|
+
) => {
|
|
289
|
+
element.addEventListener('click', onClick);
|
|
290
|
+
element.addEventListener('keydown', onKeyDown);
|
|
291
|
+
return () => {
|
|
292
|
+
element.removeEventListener('click', onClick);
|
|
293
|
+
element.removeEventListener('keydown', onKeyDown);
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Streaming markdown.
|
|
300
|
+
*
|
|
301
|
+
* ```hbs
|
|
302
|
+
* <MarkdownStream @text={{message.body}} @streaming={{message.isStreaming}}
|
|
303
|
+
* @render={{this.renderMarkdown}} @resolveLink={{this.resolve}}
|
|
304
|
+
* @onLinkActivate={{this.open}}>
|
|
305
|
+
* <:directive as |d|><CardEmbed @url={{d.body}} @format={{get d.args 0}} /></:directive>
|
|
306
|
+
* </MarkdownStream>
|
|
307
|
+
* ```
|
|
308
|
+
*
|
|
309
|
+
* Better than re-rendering the whole message per token (what the current
|
|
310
|
+
* host assistant does): frozen blocks call the renderer once, the live tail
|
|
311
|
+
* is healed so half-written emphasis and links render as intended, and a
|
|
312
|
+
* selection made mid-stream survives completion because a block whose HTML
|
|
313
|
+
* does not change is not touched.
|
|
314
|
+
*/
|
|
315
|
+
export class MarkdownStream extends Component<MarkdownStreamSignature> {
|
|
316
|
+
/** bumped when an async render resolves, so the view re-reads the cache */
|
|
317
|
+
@tracked private version = 0;
|
|
318
|
+
/** bfm references found in rendered blocks, each rendered in place
|
|
319
|
+
* through <:ref> */
|
|
320
|
+
@tracked refSlots: RefSlot[] = [];
|
|
321
|
+
private projection: Projection | undefined;
|
|
322
|
+
private rendered = new Map<string, RenderedEntry>();
|
|
323
|
+
/** numbers each render request, so results apply in request order */
|
|
324
|
+
private renderSeq = 0;
|
|
325
|
+
|
|
326
|
+
get streaming(): boolean {
|
|
327
|
+
return Boolean(this.args.streaming);
|
|
328
|
+
}
|
|
329
|
+
get flavor(): MarkdownFlavor {
|
|
330
|
+
return this.args.flavor ?? 'markdown';
|
|
331
|
+
}
|
|
332
|
+
get isBfm(): boolean {
|
|
333
|
+
return this.flavor === 'bfm';
|
|
334
|
+
}
|
|
335
|
+
private isDiagram(language: string | undefined): boolean {
|
|
336
|
+
if (!this.isBfm || !language) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
let languages = this.args.diagramLanguages ?? ['mermaid'];
|
|
340
|
+
return languages.includes(language.toLowerCase());
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
get blocks(): ViewBlock[] {
|
|
344
|
+
// consume the version so a resolved async render re-renders
|
|
345
|
+
void this.version;
|
|
346
|
+
let text = this.args.text ?? '';
|
|
347
|
+
this.projection = project(
|
|
348
|
+
this.projection,
|
|
349
|
+
text,
|
|
350
|
+
this.streaming,
|
|
351
|
+
this.flavor,
|
|
352
|
+
);
|
|
353
|
+
let definitions = collectDefinitions(text);
|
|
354
|
+
let live = new Set<string>();
|
|
355
|
+
let codeIndex = 0;
|
|
356
|
+
let view = this.projection.blocks.map((block): ViewBlock => {
|
|
357
|
+
live.add(block.key);
|
|
358
|
+
if (block.kind === 'code') {
|
|
359
|
+
let index = codeIndex++;
|
|
360
|
+
return {
|
|
361
|
+
key: block.key,
|
|
362
|
+
kind: 'code',
|
|
363
|
+
mode: block.mode,
|
|
364
|
+
code: {
|
|
365
|
+
key: block.key,
|
|
366
|
+
index,
|
|
367
|
+
source: block.source ?? '',
|
|
368
|
+
language: block.language,
|
|
369
|
+
complete: Boolean(block.complete),
|
|
370
|
+
},
|
|
371
|
+
diagram: Boolean(block.complete) && this.isDiagram(block.language),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (block.kind === 'directive') {
|
|
375
|
+
let directive = block.directive!;
|
|
376
|
+
let isRef =
|
|
377
|
+
this.isBfm &&
|
|
378
|
+
(directive.name === 'card' || directive.name === 'file');
|
|
379
|
+
return {
|
|
380
|
+
key: block.key,
|
|
381
|
+
kind: 'directive',
|
|
382
|
+
mode: block.mode,
|
|
383
|
+
directive: { key: block.key, ...directive },
|
|
384
|
+
ref: isRef
|
|
385
|
+
? {
|
|
386
|
+
key: block.key,
|
|
387
|
+
type: directive.name as 'card' | 'file',
|
|
388
|
+
url: directive.body,
|
|
389
|
+
inline: false,
|
|
390
|
+
args: directive.args,
|
|
391
|
+
complete: directive.complete,
|
|
392
|
+
}
|
|
393
|
+
: undefined,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
if (block.pendingMath) {
|
|
397
|
+
return {
|
|
398
|
+
key: block.key,
|
|
399
|
+
kind: 'text',
|
|
400
|
+
mode: block.mode,
|
|
401
|
+
pendingMath: block.raw,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
let src = block.raw;
|
|
405
|
+
if (block.mode === 'live' && (this.args.heal ?? true)) {
|
|
406
|
+
src = heal(this.isBfm ? healBfm(src) : src);
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
key: block.key,
|
|
410
|
+
kind: 'text',
|
|
411
|
+
mode: block.mode,
|
|
412
|
+
caret: block.mode === 'live',
|
|
413
|
+
html: this.safeFor(
|
|
414
|
+
block.key,
|
|
415
|
+
this.htmlFor(block.key, src, block.mode, definitions),
|
|
416
|
+
),
|
|
417
|
+
};
|
|
418
|
+
});
|
|
419
|
+
// forget blocks that no longer exist (a reset, a regenerate)
|
|
420
|
+
for (let key of this.rendered.keys()) {
|
|
421
|
+
if (!live.has(key)) {
|
|
422
|
+
this.rendered.delete(key);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return view;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** The block's HTML, rendering only when its source changed. An async
|
|
429
|
+
* renderer keeps the previous HTML on screen until a result lands. Every
|
|
430
|
+
* request is numbered, and a result replaces the HTML on screen whenever
|
|
431
|
+
* it comes from a newer request than that HTML did, even when a still
|
|
432
|
+
* newer request is pending: under a steady stream with a slow renderer,
|
|
433
|
+
* the tail then advances with each result instead of waiting for the
|
|
434
|
+
* stream to pause. A result older than the HTML on screen is dropped. */
|
|
435
|
+
private htmlFor(
|
|
436
|
+
key: string,
|
|
437
|
+
blockSrc: string,
|
|
438
|
+
mode: BlockMode,
|
|
439
|
+
definitions: string,
|
|
440
|
+
): string {
|
|
441
|
+
// the cache is keyed on what the renderer sees: a definition added later
|
|
442
|
+
// in the document re-renders the blocks that could use it
|
|
443
|
+
let src = definitions ? `${blockSrc}\u0000${definitions}` : blockSrc;
|
|
444
|
+
let entry = this.rendered.get(key);
|
|
445
|
+
if (entry && entry.src === src) {
|
|
446
|
+
return entry.html;
|
|
447
|
+
}
|
|
448
|
+
if (entry && entry.requested === src) {
|
|
449
|
+
return entry.html;
|
|
450
|
+
}
|
|
451
|
+
let render = this.args.render;
|
|
452
|
+
let seq = ++this.renderSeq;
|
|
453
|
+
if (!render) {
|
|
454
|
+
let html = renderPlain(blockSrc);
|
|
455
|
+
this.rendered.set(key, { src, html, seq });
|
|
456
|
+
return html;
|
|
457
|
+
}
|
|
458
|
+
let result = render({
|
|
459
|
+
key,
|
|
460
|
+
src: blockSrc,
|
|
461
|
+
mode,
|
|
462
|
+
flavor: this.flavor,
|
|
463
|
+
definitions,
|
|
464
|
+
});
|
|
465
|
+
if (typeof result === 'string') {
|
|
466
|
+
this.rendered.set(key, { src, html: result, seq });
|
|
467
|
+
return result;
|
|
468
|
+
}
|
|
469
|
+
let previous = entry?.html ?? '';
|
|
470
|
+
this.rendered.set(key, {
|
|
471
|
+
src: entry?.src ?? '',
|
|
472
|
+
html: previous,
|
|
473
|
+
seq: entry?.seq ?? 0,
|
|
474
|
+
safe: entry?.safe,
|
|
475
|
+
requested: src,
|
|
476
|
+
});
|
|
477
|
+
let land = (html: string) => {
|
|
478
|
+
let current = this.rendered.get(key);
|
|
479
|
+
if (!current || seq <= current.seq || this.isDestroyed) {
|
|
480
|
+
return; // the block is gone, or newer HTML is already on screen
|
|
481
|
+
}
|
|
482
|
+
this.rendered.set(key, {
|
|
483
|
+
src,
|
|
484
|
+
html,
|
|
485
|
+
seq,
|
|
486
|
+
safe: current.safe,
|
|
487
|
+
// a newer request still pending stays pending
|
|
488
|
+
requested: current.requested === src ? undefined : current.requested,
|
|
489
|
+
});
|
|
490
|
+
this.version++;
|
|
491
|
+
};
|
|
492
|
+
result.then(land, () => land(renderPlain(blockSrc)));
|
|
493
|
+
return previous;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private safeFor(key: string, html: string): SafeString {
|
|
497
|
+
let entry = this.rendered.get(key);
|
|
498
|
+
if (entry?.safe && entry.safe.toString() === html) {
|
|
499
|
+
return entry.safe;
|
|
500
|
+
}
|
|
501
|
+
let safe = htmlSafe(html);
|
|
502
|
+
if (entry) {
|
|
503
|
+
entry.safe = safe;
|
|
504
|
+
}
|
|
505
|
+
return safe;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
get announcement(): string {
|
|
509
|
+
return this.streaming ? '' : (this.args.announceComplete ?? '');
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ── links ──────────────────────────────────────────────────────────────
|
|
513
|
+
|
|
514
|
+
/** Runs whenever a text block's HTML is (re)inserted: marks external
|
|
515
|
+
* links to open in a new tab, and asks @resolveLink about relative links
|
|
516
|
+
* and path-like inline code. Unconfirmed, they stay plain text. */
|
|
517
|
+
linkify = (element: HTMLElement, blockKey: string) => {
|
|
518
|
+
if (this.isBfm) {
|
|
519
|
+
this.collectRefs(element, blockKey);
|
|
520
|
+
}
|
|
521
|
+
for (let anchor of Array.from(element.querySelectorAll('a[href]'))) {
|
|
522
|
+
let href = anchor.getAttribute('href') ?? '';
|
|
523
|
+
if (isExternalHref(href)) {
|
|
524
|
+
anchor.setAttribute('target', '_blank');
|
|
525
|
+
anchor.setAttribute('rel', 'noopener noreferrer');
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
this.pend(anchor, href, 'url');
|
|
529
|
+
}
|
|
530
|
+
for (let code of Array.from(element.querySelectorAll('code'))) {
|
|
531
|
+
if (code.closest('pre')) {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
let text = code.textContent ?? '';
|
|
535
|
+
if (isPathLike(text)) {
|
|
536
|
+
this.pend(code, text.trim(), 'path');
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
/** Register the block's reference placeholders as slots. Deferred past
|
|
542
|
+
* the render that inserted them: refSlots was read in that render. */
|
|
543
|
+
private collectRefs(element: HTMLElement, blockKey: string) {
|
|
544
|
+
let found: RefSlot[] = [];
|
|
545
|
+
Array.from(element.querySelectorAll(REF_SELECTOR)).forEach(
|
|
546
|
+
(placeholder, index) => {
|
|
547
|
+
let id = `${blockKey}#${index}`;
|
|
548
|
+
let ref = refFromPlaceholder(placeholder, id);
|
|
549
|
+
if (ref) {
|
|
550
|
+
found.push({ id, blockKey, element: placeholder, ref });
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
);
|
|
554
|
+
// refSlots is read only inside the microtask: reading it here would make
|
|
555
|
+
// this modifier depend on it, and the write below would re-run the
|
|
556
|
+
// modifier forever
|
|
557
|
+
queueMicrotask(() => {
|
|
558
|
+
if (this.isDestroying || this.isDestroyed) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
let hadAny = this.refSlots.some((slot) => slot.blockKey === blockKey);
|
|
562
|
+
if (!found.length && !hadAny) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
this.refSlots = [
|
|
566
|
+
...this.refSlots.filter(
|
|
567
|
+
(slot) => slot.blockKey !== blockKey && slot.element.isConnected,
|
|
568
|
+
),
|
|
569
|
+
...found,
|
|
570
|
+
];
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private pend(el: Element, href: string, kind: 'url' | 'path') {
|
|
575
|
+
let resolve = this.args.resolveLink;
|
|
576
|
+
el.setAttribute(LINK_STATE, 'pending');
|
|
577
|
+
if (!resolve) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
resolve(href, kind).then(
|
|
581
|
+
(link) => {
|
|
582
|
+
if (!link || !el.isConnected) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
resolvedLinks.set(el, link);
|
|
586
|
+
el.setAttribute(LINK_STATE, 'resolved');
|
|
587
|
+
if (el.tagName !== 'A') {
|
|
588
|
+
el.setAttribute('role', 'link');
|
|
589
|
+
el.setAttribute('tabindex', '0');
|
|
590
|
+
}
|
|
591
|
+
},
|
|
592
|
+
() => undefined,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private activate(event: MouseEvent | KeyboardEvent) {
|
|
597
|
+
let target = event.target instanceof Element ? event.target : null;
|
|
598
|
+
let el = target?.closest(`[${LINK_STATE}]`);
|
|
599
|
+
if (!el) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (el.getAttribute(LINK_STATE) !== 'resolved') {
|
|
603
|
+
// an unconfirmed relative link goes nowhere
|
|
604
|
+
event.preventDefault();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
let link = resolvedLinks.get(el);
|
|
608
|
+
if (link) {
|
|
609
|
+
event.preventDefault();
|
|
610
|
+
this.args.onLinkActivate?.(link, event);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
onClick = (event: MouseEvent) => this.activate(event);
|
|
615
|
+
onKeyDown = (event: KeyboardEvent) => {
|
|
616
|
+
if (event.key === 'Enter') {
|
|
617
|
+
this.activate(event);
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
<template>
|
|
622
|
+
<div
|
|
623
|
+
class='pretui-md'
|
|
624
|
+
aria-busy={{if this.streaming 'true'}}
|
|
625
|
+
data-streaming={{if this.streaming 'true' 'false'}}
|
|
626
|
+
{{listen this.onClick this.onKeyDown}}
|
|
627
|
+
data-test-pretui-markdown-stream
|
|
628
|
+
...attributes
|
|
629
|
+
>
|
|
630
|
+
<MarkdownSurface class='pretui-md-prose'>
|
|
631
|
+
{{#each this.blocks key='key' as |block|}}
|
|
632
|
+
{{#if block.code}}
|
|
633
|
+
<div
|
|
634
|
+
class='pretui-md-block'
|
|
635
|
+
data-scroll-anchor
|
|
636
|
+
data-kind='code'
|
|
637
|
+
data-mode={{block.mode}}
|
|
638
|
+
data-complete={{if block.code.complete 'true' 'false'}}
|
|
639
|
+
data-test-pretui-markdown-block='code'
|
|
640
|
+
>
|
|
641
|
+
{{#if (bothTrue block.diagram (has-block 'diagram'))}}
|
|
642
|
+
{{yield block.code to='diagram'}}
|
|
643
|
+
{{else if (has-block 'code')}}
|
|
644
|
+
{{yield block.code to='code'}}
|
|
645
|
+
{{else}}
|
|
646
|
+
<CodeBlock
|
|
647
|
+
@code={{block.code.source}}
|
|
648
|
+
@language={{block.code.language}}
|
|
649
|
+
@maxLines={{24}}
|
|
650
|
+
data-scroll-region
|
|
651
|
+
/>
|
|
652
|
+
{{/if}}
|
|
653
|
+
</div>
|
|
654
|
+
{{else if block.directive}}
|
|
655
|
+
<div
|
|
656
|
+
class='pretui-md-block'
|
|
657
|
+
data-scroll-anchor
|
|
658
|
+
data-kind='directive'
|
|
659
|
+
data-mode={{block.mode}}
|
|
660
|
+
data-test-pretui-markdown-block='directive'
|
|
661
|
+
>
|
|
662
|
+
{{#if (bothTrue block.ref (has-block 'ref'))}}
|
|
663
|
+
{{yield block.ref to='ref'}}
|
|
664
|
+
{{else if (has-block 'directive')}}
|
|
665
|
+
{{yield block.directive to='directive'}}
|
|
666
|
+
{{else}}
|
|
667
|
+
<p>::{{block.directive.name}}[{{block.directive.body}}]</p>
|
|
668
|
+
{{/if}}
|
|
669
|
+
</div>
|
|
670
|
+
{{else if block.pendingMath}}
|
|
671
|
+
<div
|
|
672
|
+
class='pretui-md-block'
|
|
673
|
+
data-scroll-anchor
|
|
674
|
+
data-kind='text'
|
|
675
|
+
data-mode={{block.mode}}
|
|
676
|
+
data-test-pretui-markdown-block='text'
|
|
677
|
+
><pre
|
|
678
|
+
class='pretui-md-math-pending'
|
|
679
|
+
data-pending-math
|
|
680
|
+
data-test-pretui-markdown-pending-math
|
|
681
|
+
>{{block.pendingMath}}</pre></div>
|
|
682
|
+
{{else}}
|
|
683
|
+
<div
|
|
684
|
+
class='pretui-md-block'
|
|
685
|
+
data-scroll-anchor
|
|
686
|
+
data-kind='text'
|
|
687
|
+
data-mode={{block.mode}}
|
|
688
|
+
{{linkified this.linkify block.html block.key}}
|
|
689
|
+
{{caretAtEnd block.html block.caret}}
|
|
690
|
+
data-test-pretui-markdown-block='text'
|
|
691
|
+
>{{block.html}}</div>
|
|
692
|
+
{{/if}}
|
|
693
|
+
{{/each}}
|
|
694
|
+
{{#if (has-block 'ref')}}
|
|
695
|
+
{{#each this.refSlots key='id' as |slot|}}
|
|
696
|
+
{{#in-element slot.element}}
|
|
697
|
+
{{yield slot.ref to='ref'}}
|
|
698
|
+
{{/in-element}}
|
|
699
|
+
{{/each}}
|
|
700
|
+
{{/if}}
|
|
701
|
+
</MarkdownSurface>
|
|
702
|
+
<span
|
|
703
|
+
class='pretui-md-live'
|
|
704
|
+
role='status'
|
|
705
|
+
aria-live='polite'
|
|
706
|
+
data-test-pretui-markdown-announce
|
|
707
|
+
>{{this.announcement}}</span>
|
|
708
|
+
</div>
|
|
709
|
+
<style scoped>
|
|
710
|
+
@layer PretComponent, PretComposite;
|
|
711
|
+
@layer PretComposite {
|
|
712
|
+
/* the answer's typography is MarkdownSurface's: the reading voice,
|
|
713
|
+
the heading ladder, code and tables; this keeps the blocks' own
|
|
714
|
+
rhythm and the stream's marks */
|
|
715
|
+
.pretui-md {
|
|
716
|
+
min-width: 0;
|
|
717
|
+
}
|
|
718
|
+
.pretui-md-block {
|
|
719
|
+
min-width: 0;
|
|
720
|
+
}
|
|
721
|
+
/* Each block wraps the HTML its renderer returned and adds no
|
|
722
|
+
spacing of its own: the block has no padding or border, so its
|
|
723
|
+
children's margins collapse through it and Prose's margins
|
|
724
|
+
between paragraphs and around headings hold exactly as in a
|
|
725
|
+
static render. Prose zeroes the outer edges of its own first and
|
|
726
|
+
last child, which here are blocks, so the same edges are zeroed
|
|
727
|
+
one level down. */
|
|
728
|
+
.pretui-md-block:first-child > :deep(:first-child) {
|
|
729
|
+
margin-block-start: 0;
|
|
730
|
+
}
|
|
731
|
+
.pretui-md-block:last-child > :deep(:last-child) {
|
|
732
|
+
margin-block-end: 0;
|
|
733
|
+
}
|
|
734
|
+
.pretui-md-block[data-kind='code'] {
|
|
735
|
+
margin-block: var(--boxel-sp-sm);
|
|
736
|
+
}
|
|
737
|
+
.pretui-md-block[data-kind='code']:first-child {
|
|
738
|
+
margin-block-start: 0;
|
|
739
|
+
}
|
|
740
|
+
.pretui-md-block[data-kind='code']:last-child {
|
|
741
|
+
margin-block-end: 0;
|
|
742
|
+
}
|
|
743
|
+
/* an unconfirmed relative link reads as the plain text it is */
|
|
744
|
+
.pretui-md-block :deep([data-markdown-link='pending']) {
|
|
745
|
+
color: inherit;
|
|
746
|
+
text-decoration: none;
|
|
747
|
+
cursor: text;
|
|
748
|
+
}
|
|
749
|
+
.pretui-md-block :deep(code[data-markdown-link='resolved']) {
|
|
750
|
+
cursor: pointer;
|
|
751
|
+
text-decoration: underline;
|
|
752
|
+
text-decoration-color: var(--muted-foreground);
|
|
753
|
+
text-underline-offset: 0.18em;
|
|
754
|
+
}
|
|
755
|
+
.pretui-md-block
|
|
756
|
+
:deep(code[data-markdown-link='resolved']:focus-visible) {
|
|
757
|
+
outline: 2px solid var(--ring);
|
|
758
|
+
outline-offset: 2px;
|
|
759
|
+
}
|
|
760
|
+
.pretui-md-block :deep(.pretui-md-caret) {
|
|
761
|
+
display: inline-block;
|
|
762
|
+
width: 0.5em;
|
|
763
|
+
height: 1em;
|
|
764
|
+
margin-inline-start: var(--boxel-sp-6xs);
|
|
765
|
+
vertical-align: text-bottom;
|
|
766
|
+
border-radius: var(--boxel-border-radius-2xs);
|
|
767
|
+
background-color: color-mix(in oklch, var(--foreground) 60%, transparent);
|
|
768
|
+
animation: pretui-md-caret 1s steps(2, start) infinite;
|
|
769
|
+
}
|
|
770
|
+
@keyframes pretui-md-caret {
|
|
771
|
+
to {
|
|
772
|
+
visibility: hidden;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/* math still arriving is its source, a pasteable string, in the
|
|
776
|
+
quieter ink rather than faded */
|
|
777
|
+
.pretui-md-math-pending {
|
|
778
|
+
font-family: var(--font-mono);
|
|
779
|
+
font-size: var(--boxel-font-size-xs);
|
|
780
|
+
line-height: var(--boxel-line-height-xs);
|
|
781
|
+
color: var(--muted-foreground);
|
|
782
|
+
}
|
|
783
|
+
.pretui-md-live {
|
|
784
|
+
position: absolute;
|
|
785
|
+
width: 1px;
|
|
786
|
+
height: 1px;
|
|
787
|
+
overflow: hidden;
|
|
788
|
+
clip-path: inset(50%);
|
|
789
|
+
white-space: nowrap;
|
|
790
|
+
}
|
|
791
|
+
@media (prefers-reduced-motion: reduce) {
|
|
792
|
+
.pretui-md-block :deep(.pretui-md-caret) {
|
|
793
|
+
animation: none;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
</style>
|
|
798
|
+
</template>
|
|
799
|
+
}
|