@itookit/dsht 0.3.0 → 0.3.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/README.i18n.yaml +2 -2
- package/README.md +39 -23
- package/README.zh.md +40 -24
- package/dist/catalog/controller.d.ts +32 -0
- package/dist/catalog/controller.js +88 -0
- package/dist/catalog/index.d.ts +2 -0
- package/dist/catalog/index.js +2 -0
- package/dist/{cli.js → cli/index.js} +42 -29
- package/dist/controller/connection.d.ts +80 -0
- package/dist/controller/connection.js +190 -0
- package/dist/controller/controller.d.ts +269 -0
- package/dist/controller/controller.js +372 -0
- package/dist/controller/index.d.ts +5 -0
- package/dist/controller/index.js +3 -0
- package/dist/controller/memory-log.d.ts +35 -0
- package/dist/controller/memory-log.js +95 -0
- package/dist/cost/config.d.ts +17 -0
- package/dist/cost/config.js +68 -0
- package/dist/cost/controller.d.ts +41 -0
- package/dist/cost/controller.js +115 -0
- package/dist/cost/index.d.ts +10 -0
- package/dist/cost/index.js +8 -0
- package/dist/cost/ledger-files.d.ts +16 -0
- package/dist/cost/ledger-files.js +103 -0
- package/dist/cost/ledger.d.ts +78 -0
- package/dist/cost/ledger.js +146 -0
- package/dist/cost/pricing.d.ts +86 -0
- package/dist/cost/pricing.js +223 -0
- package/dist/cost/records.d.ts +17 -0
- package/dist/cost/records.js +79 -0
- package/dist/cost/scanner.d.ts +22 -0
- package/dist/cost/scanner.js +95 -0
- package/dist/cost/types.d.ts +85 -0
- package/dist/cost/types.js +7 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/session/connection-view.d.ts +18 -0
- package/dist/session/connection-view.js +1 -0
- package/dist/{controller.d.ts → session/controller.d.ts} +102 -136
- package/dist/session/controller.js +616 -0
- package/dist/session/export-html.d.ts +9 -0
- package/dist/session/export-html.js +39 -0
- package/dist/{export.d.ts → session/export.d.ts} +1 -1
- package/dist/{export.js → session/export.js} +6 -20
- package/dist/{history.d.ts → session/history.d.ts} +17 -0
- package/dist/{history.js → session/history.js} +163 -2
- package/dist/session/index.d.ts +17 -0
- package/dist/session/index.js +10 -0
- package/dist/session/markdown.d.ts +39 -0
- package/dist/session/markdown.js +255 -0
- package/dist/session/math.d.ts +11 -0
- package/dist/session/math.js +82 -0
- package/dist/session/navigation.d.ts +37 -0
- package/dist/session/navigation.js +84 -0
- package/dist/{references.js → session/references.js} +1 -1
- package/dist/{telemetry.d.ts → session/telemetry.d.ts} +1 -1
- package/dist/{telemetry.js → session/telemetry.js} +1 -1
- package/dist/{transcript.d.ts → session/transcript.d.ts} +65 -1
- package/dist/{transcript.js → session/transcript.js} +141 -20
- package/dist/session/types.d.ts +18 -0
- package/dist/session/types.js +2 -0
- package/dist/state.d.ts +41 -0
- package/dist/state.js +9 -0
- package/dist/storage/directories.d.ts +14 -0
- package/dist/storage/directories.js +24 -0
- package/dist/storage/files.d.ts +51 -0
- package/dist/storage/files.js +152 -0
- package/dist/storage/heap-snapshot.d.ts +19 -0
- package/dist/storage/heap-snapshot.js +29 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +4 -0
- package/dist/transport/auth.js +65 -0
- package/dist/transport/host.d.ts +13 -0
- package/dist/transport/host.js +1 -0
- package/dist/ui/app.d.ts +11 -0
- package/dist/ui/app.js +790 -0
- package/dist/ui/chat/header.d.ts +11 -0
- package/dist/ui/chat/header.js +14 -0
- package/dist/ui/chat/history-view.d.ts +12 -0
- package/dist/ui/chat/history-view.js +17 -0
- package/dist/ui/chat/status.d.ts +91 -0
- package/dist/ui/chat/status.js +386 -0
- package/dist/ui/chat/viewport.d.ts +17 -0
- package/dist/ui/chat/viewport.js +14 -0
- package/dist/ui/commands/parse.d.ts +99 -0
- package/dist/ui/commands/parse.js +126 -0
- package/dist/ui/commands/registry.d.ts +33 -0
- package/dist/ui/commands/registry.js +73 -0
- package/dist/ui/copy-mode.d.ts +4 -0
- package/dist/ui/copy-mode.js +6 -0
- package/dist/{cost-view.d.ts → ui/dialogs/cost.d.ts} +1 -1
- package/dist/{cost-view.js → ui/dialogs/cost.js} +6 -6
- package/dist/ui/dialogs/index.d.ts +120 -0
- package/dist/ui/dialogs/index.js +113 -0
- package/dist/ui/dialogs/picker.d.ts +18 -0
- package/dist/ui/dialogs/picker.js +38 -0
- package/dist/ui/frozen.d.ts +8 -0
- package/dist/ui/frozen.js +7 -0
- package/dist/ui/input/references.d.ts +12 -0
- package/dist/ui/input/references.js +15 -0
- package/dist/ui/mount.d.ts +6 -0
- package/dist/ui/mount.js +11 -0
- package/dist/{theme.d.ts → ui/theme/index.d.ts} +1 -1
- package/package.json +19 -13
- package/dist/app.d.ts +0 -21
- package/dist/app.js +0 -805
- package/dist/auth.js +0 -108
- package/dist/controller.js +0 -961
- package/dist/cost.d.ts +0 -119
- package/dist/cost.js +0 -313
- package/dist/history-view.d.ts +0 -8
- package/dist/history-view.js +0 -12
- package/dist/navigation.d.ts +0 -11
- package/dist/navigation.js +0 -36
- package/dist/status.d.ts +0 -28
- package/dist/status.js +0 -157
- /package/dist/{cli.d.ts → cli/index.d.ts} +0 -0
- /package/dist/{memory.d.ts → session/memory.d.ts} +0 -0
- /package/dist/{memory.js → session/memory.js} +0 -0
- /package/dist/{references.d.ts → session/references.d.ts} +0 -0
- /package/dist/{auth.d.ts → transport/auth.d.ts} +0 -0
- /package/dist/{client.d.ts → transport/client.d.ts} +0 -0
- /package/dist/{client.js → transport/client.js} +0 -0
- /package/dist/{endpoint.d.ts → transport/endpoint.d.ts} +0 -0
- /package/dist/{endpoint.js → transport/endpoint.js} +0 -0
- /package/dist/{wire.d.ts → transport/wire.d.ts} +0 -0
- /package/dist/{wire.js → transport/wire.js} +0 -0
- /package/dist/{input-history.d.ts → ui/input/history.d.ts} +0 -0
- /package/dist/{input-history.js → ui/input/history.js} +0 -0
- /package/dist/{input.d.ts → ui/input/input.d.ts} +0 -0
- /package/dist/{input.js → ui/input/input.js} +0 -0
- /package/dist/{mouse.d.ts → ui/input/mouse.d.ts} +0 -0
- /package/dist/{mouse.js → ui/input/mouse.js} +0 -0
- /package/dist/{theme.js → ui/theme/index.js} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Client } from '
|
|
1
|
+
import type { Client } from '../transport/client.ts';
|
|
2
2
|
/** Save an archive without overwriting an existing file or retaining a partial download.
|
|
3
3
|
* @param client - Authenticated host transport.
|
|
4
4
|
* @param sessionId - Selected session to export.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Stream authenticated session archives to exclusive local files. */
|
|
2
|
-
import { open, unlink } from 'node:fs/promises';
|
|
3
2
|
import { resolve } from 'node:path';
|
|
3
|
+
import { writeExclusiveStream } from "../storage/index.js";
|
|
4
4
|
/** Save an archive without overwriting an existing file or retaining a partial download.
|
|
5
5
|
* @param client - Authenticated host transport.
|
|
6
6
|
* @param sessionId - Selected session to export.
|
|
@@ -10,14 +10,12 @@ import { resolve } from 'node:path';
|
|
|
10
10
|
*/
|
|
11
11
|
export async function saveSessionLog(client, sessionId, destination, signal) {
|
|
12
12
|
const path = resolve(destination ?? `session-${sessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}-${Date.now()}.zip`);
|
|
13
|
-
|
|
14
|
-
let complete = false;
|
|
15
|
-
try {
|
|
13
|
+
await writeExclusiveStream(path, async () => {
|
|
16
14
|
const response = await client.sessionLog(sessionId, signal);
|
|
17
15
|
if (!response.body)
|
|
18
16
|
throw new Error('Session log export has no body');
|
|
19
17
|
const reader = response.body.getReader();
|
|
20
|
-
async function* chunks() {
|
|
18
|
+
return (async function* chunks() {
|
|
21
19
|
try {
|
|
22
20
|
while (true) {
|
|
23
21
|
const chunk = await reader.read();
|
|
@@ -34,19 +32,7 @@ export async function saveSessionLog(client, sessionId, destination, signal) {
|
|
|
34
32
|
reader.releaseLock();
|
|
35
33
|
}
|
|
36
34
|
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
complete = true;
|
|
41
|
-
return path;
|
|
42
|
-
}
|
|
43
|
-
finally {
|
|
44
|
-
try {
|
|
45
|
-
await file.close();
|
|
46
|
-
}
|
|
47
|
-
finally {
|
|
48
|
-
if (!complete)
|
|
49
|
-
await unlink(path);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
35
|
+
})();
|
|
36
|
+
}, signal);
|
|
37
|
+
return path;
|
|
52
38
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Message, type MessagePart, type Transcript } from './transcript.ts';
|
|
2
|
+
import { type MarkdownSpan } from './markdown.ts';
|
|
2
3
|
/** Default fold mode; individual sequence overrides are view state, never stored content. */
|
|
3
4
|
export type Reasoning = 'row' | 'full';
|
|
4
5
|
/** Color semantics are independent of the selected terminal palette. */
|
|
@@ -9,11 +10,27 @@ export interface HistoryRow {
|
|
|
9
10
|
kind: RowKind;
|
|
10
11
|
bold?: boolean;
|
|
11
12
|
seq?: number;
|
|
13
|
+
spans?: MarkdownSpan[];
|
|
12
14
|
}
|
|
13
15
|
/** Drop all terminal rows and layout metadata for an evicted or inactive transcript.
|
|
14
16
|
* @param transcript - Transcript whose previously returned layout is no longer used.
|
|
15
17
|
*/
|
|
16
18
|
export declare function releaseHistoryLayout(transcript: Transcript): void;
|
|
19
|
+
/** Report what one transcript's layout still holds, for memory samples and diagnostics.
|
|
20
|
+
*
|
|
21
|
+
* The row cache is the only structure that grows with expanded reasoning and Markdown rows, and
|
|
22
|
+
* its byte bound counts neither the span objects nor the incremental live-tail state.
|
|
23
|
+
* @param transcript - Transcript whose layout should be measured.
|
|
24
|
+
* @returns Cache and live-tail counters, or undefined when no layout was built yet.
|
|
25
|
+
*/
|
|
26
|
+
export declare function layoutStats(transcript: Transcript): {
|
|
27
|
+
rows: number;
|
|
28
|
+
cacheBytes: number;
|
|
29
|
+
spans: number;
|
|
30
|
+
spanChars: number;
|
|
31
|
+
liveWraps: number;
|
|
32
|
+
liveMarkdown: number;
|
|
33
|
+
} | undefined;
|
|
17
34
|
/** Lay out an indexed conversation without concatenating its historical rows on every stream frame.
|
|
18
35
|
* @param transcript - Selected session's semantic content and unfinished assistant output.
|
|
19
36
|
* @param width - Available terminal columns.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Indexed semantic history with bounded terminal-row caching and viewport-only materialization. */
|
|
2
2
|
import wrapAnsi from 'wrap-ansi';
|
|
3
3
|
import { toolLine } from "./transcript.js";
|
|
4
|
+
import { hasMarkdown, markdownRows } from "./markdown.js";
|
|
4
5
|
/** A session owns one layout cache; dropped sessions release their entire cache. */
|
|
5
6
|
class LayoutIndex {
|
|
6
7
|
width;
|
|
@@ -15,6 +16,9 @@ class LayoutIndex {
|
|
|
15
16
|
// Bound both row objects and long rows. Full semantic content remains available for search.
|
|
16
17
|
maxRows = 2048;
|
|
17
18
|
heights = new WeakMap();
|
|
19
|
+
/** Incremental wrapping state for the growing live tail, keyed by each live part's identity. */
|
|
20
|
+
liveWraps = new Map();
|
|
21
|
+
liveMarkdown = new Map();
|
|
18
22
|
constructor(width, reasoning, overrides) {
|
|
19
23
|
this.width = width;
|
|
20
24
|
this.reasoning = reasoning;
|
|
@@ -51,8 +55,26 @@ class LayoutIndex {
|
|
|
51
55
|
this.cacheSize = 0;
|
|
52
56
|
this.length = 0;
|
|
53
57
|
this.heights = new WeakMap();
|
|
58
|
+
this.liveWraps.clear();
|
|
59
|
+
this.liveMarkdown.clear();
|
|
54
60
|
}
|
|
55
61
|
get cachedRowCount() { return this.cacheSize; }
|
|
62
|
+
/** Measure what the row cache actually holds, including the style spans the byte bound ignores.
|
|
63
|
+
* @returns Cached rows, their accounted characters, and the span objects inside them.
|
|
64
|
+
*/
|
|
65
|
+
stats() {
|
|
66
|
+
let cacheBytes = 0, spans = 0, spanChars = 0;
|
|
67
|
+
for (const { rows } of this.cache.values()) {
|
|
68
|
+
for (const row of rows) {
|
|
69
|
+
cacheBytes += row.text.length;
|
|
70
|
+
for (const span of row.spans ?? []) {
|
|
71
|
+
spans++;
|
|
72
|
+
spanChars += span.text.length;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { rows: this.cacheSize, cacheBytes, spans, spanChars };
|
|
77
|
+
}
|
|
56
78
|
get assistantSeen() { return this.segments.at(-1)?.assistantSeen ?? false; }
|
|
57
79
|
update(messages) {
|
|
58
80
|
if (messages === this.messages)
|
|
@@ -111,6 +133,108 @@ class LayoutIndex {
|
|
|
111
133
|
}
|
|
112
134
|
}
|
|
113
135
|
const liveRows = new WeakMap();
|
|
136
|
+
/** Wrap text into rows with the per-kind trimming rule.
|
|
137
|
+
* @param text - Text to wrap.
|
|
138
|
+
* @param width - Available terminal columns.
|
|
139
|
+
* @param kind - Part kind, which decides whether wrapped lines are trimmed.
|
|
140
|
+
* @param seq - Durable sequence, for committed parts.
|
|
141
|
+
* @returns The wrapped rows.
|
|
142
|
+
*/
|
|
143
|
+
function wrapRows(text, width, kind, seq) {
|
|
144
|
+
return wrapAnsi(text, width, { hard: true, trim: !['tool', 'success', 'error'].includes(kind) })
|
|
145
|
+
.split('\n').map(text => ({ text, kind, seq }));
|
|
146
|
+
}
|
|
147
|
+
/** Bound the source inspected for a folded row, which renders only the first `width` columns.
|
|
148
|
+
* @param text - Complete live text.
|
|
149
|
+
* @param width - Available terminal columns.
|
|
150
|
+
* @returns A prefix that cannot change the folded row.
|
|
151
|
+
*/
|
|
152
|
+
function foldedSource(text, width) {
|
|
153
|
+
let limit = width * 4 + 64;
|
|
154
|
+
while (limit < text.length && toolLine(text.slice(0, limit), width).length < width)
|
|
155
|
+
limit *= 2;
|
|
156
|
+
return limit >= text.length ? text : text.slice(0, limit);
|
|
157
|
+
}
|
|
158
|
+
/** Locate the start of one rendered row inside the source it was wrapped from.
|
|
159
|
+
*
|
|
160
|
+
* Trim removes the whitespace runs at both ends of a row, so a rendered row is not a contiguous
|
|
161
|
+
* slice; matching it backwards while skipping source whitespace recovers where it began. The scan
|
|
162
|
+
* gives up once it would have to skip more than `limit` characters, which sends the caller back to
|
|
163
|
+
* a whole-text wrap instead of scanning an unbounded whitespace run.
|
|
164
|
+
* @param pending - Source text the row was wrapped from.
|
|
165
|
+
* @param row - One rendered row from that wrap.
|
|
166
|
+
* @param limit - Maximum characters the backward scan may skip.
|
|
167
|
+
* @returns The row's start offset, or undefined when it cannot be located.
|
|
168
|
+
*/
|
|
169
|
+
function rawStart(pending, row, limit) {
|
|
170
|
+
let source = pending.length - 1;
|
|
171
|
+
let target = row.length - 1;
|
|
172
|
+
const floor = pending.length - limit;
|
|
173
|
+
while (target >= 0 && source >= floor) {
|
|
174
|
+
if (pending[source] === row[target]) {
|
|
175
|
+
source--;
|
|
176
|
+
target--;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (/\s/u.test(pending[source])) {
|
|
180
|
+
source--;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
return target < 0 ? source + 1 : undefined;
|
|
186
|
+
}
|
|
187
|
+
/** Wrap one live part incrementally, re-wrapping only rows that can still change plus the new delta.
|
|
188
|
+
*
|
|
189
|
+
* A streaming part only grows, so every row before its last non-empty one is final. The state keeps
|
|
190
|
+
* those rows plus the raw source of the unfinished remainder, and re-anchors from the whole text
|
|
191
|
+
* only when that remainder cannot be located or outgrows a few widths.
|
|
192
|
+
* @param part - One live part carrying a stable `key`.
|
|
193
|
+
* @param state - Per-layout state keyed by that identity.
|
|
194
|
+
* @param width - Available terminal columns.
|
|
195
|
+
* @param reasoning - Fold mode for completed live reasoning.
|
|
196
|
+
* @returns The part's rows.
|
|
197
|
+
*/
|
|
198
|
+
function livePartRows(part, state, width, reasoning) {
|
|
199
|
+
const key = part.key;
|
|
200
|
+
if (key === undefined)
|
|
201
|
+
return partRows([part], width, reasoning);
|
|
202
|
+
if (part.kind === 'reasoning' && reasoning === 'row' && (part.closed || width < 60)) {
|
|
203
|
+
state.delete(key);
|
|
204
|
+
return wrapRows(toolLine(`◇ /think live · ${foldedSource(part.text, width).slice(2)}`, width), width, 'reasoning');
|
|
205
|
+
}
|
|
206
|
+
const previous = state.get(key);
|
|
207
|
+
const extend = previous !== undefined && previous.width === width && previous.reasoning === reasoning
|
|
208
|
+
&& previous.kind === part.kind && part.text.length >= previous.length;
|
|
209
|
+
const trim = !['tool', 'success', 'error'].includes(part.kind);
|
|
210
|
+
const limit = width * 8 + 512;
|
|
211
|
+
const attempt = (source, prior) => {
|
|
212
|
+
const wrapped = wrapAnsi(source, width, { hard: true, trim }).split('\n');
|
|
213
|
+
const filled = wrapped.reduce((found, text, index) => text === '' ? found : index, -1);
|
|
214
|
+
if (filled < 0)
|
|
215
|
+
return { rows: [], tail: wrapped, carry: source };
|
|
216
|
+
const start = rawStart(source, wrapped[filled], limit);
|
|
217
|
+
if (start === undefined)
|
|
218
|
+
return undefined;
|
|
219
|
+
return {
|
|
220
|
+
rows: [...prior, ...wrapped.slice(0, filled).map(text => ({ text, kind: part.kind }))],
|
|
221
|
+
tail: wrapped.slice(filled),
|
|
222
|
+
// The last non-empty row and everything after it, including trailing empty rows, stay unfinished.
|
|
223
|
+
carry: source.slice(start),
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
let result = attempt(extend ? previous.carry + part.text.slice(previous.length) : part.text, extend ? previous.rows : []);
|
|
227
|
+
if (result === undefined || result.carry.length > width * 4 + 64)
|
|
228
|
+
result = attempt(part.text, []);
|
|
229
|
+
// Recovery can fail on text whose last row cannot be located; the plain one-shot wrap is the fallback.
|
|
230
|
+
if (result === undefined) {
|
|
231
|
+
state.delete(key);
|
|
232
|
+
return partRows([part], width, reasoning);
|
|
233
|
+
}
|
|
234
|
+
const rows = [...result.rows, ...result.tail.map(text => ({ text, kind: part.kind }))];
|
|
235
|
+
state.set(key, { width, reasoning, kind: part.kind, length: part.text.length, rows: result.rows, carry: result.carry });
|
|
236
|
+
return rows;
|
|
237
|
+
}
|
|
114
238
|
function partRows(parts, width, reasoning, seq) {
|
|
115
239
|
return parts.flatMap(part => {
|
|
116
240
|
const cached = seq === undefined ? liveRows.get(part) : undefined;
|
|
@@ -118,7 +242,8 @@ function partRows(parts, width, reasoning, seq) {
|
|
|
118
242
|
return cached.rows;
|
|
119
243
|
const fold = part.kind === 'reasoning' && reasoning === 'row' && (seq !== undefined || part.closed || width < 60);
|
|
120
244
|
const text = fold ? toolLine(`◇ /think${seq === undefined ? ' live' : ` ${seq}`} · ${part.text.slice(2)}`, width) : part.text;
|
|
121
|
-
const rows =
|
|
245
|
+
const rows = part.kind === 'text' ? markdownRows(text, width).map(row => ({ ...row, kind: part.kind, seq }))
|
|
246
|
+
: wrapRows(text, width, part.kind, seq);
|
|
122
247
|
if (seq === undefined)
|
|
123
248
|
liveRows.set(part, { width, reasoning, rows });
|
|
124
249
|
return rows;
|
|
@@ -142,6 +267,19 @@ export function releaseHistoryLayout(transcript) {
|
|
|
142
267
|
indexes.get(transcript)?.dispose();
|
|
143
268
|
indexes.delete(transcript);
|
|
144
269
|
}
|
|
270
|
+
/** Report what one transcript's layout still holds, for memory samples and diagnostics.
|
|
271
|
+
*
|
|
272
|
+
* The row cache is the only structure that grows with expanded reasoning and Markdown rows, and
|
|
273
|
+
* its byte bound counts neither the span objects nor the incremental live-tail state.
|
|
274
|
+
* @param transcript - Transcript whose layout should be measured.
|
|
275
|
+
* @returns Cache and live-tail counters, or undefined when no layout was built yet.
|
|
276
|
+
*/
|
|
277
|
+
export function layoutStats(transcript) {
|
|
278
|
+
const index = indexes.get(transcript);
|
|
279
|
+
if (!index)
|
|
280
|
+
return undefined;
|
|
281
|
+
return { ...index.stats(), liveWraps: index.liveWraps.size, liveMarkdown: index.liveMarkdown.size };
|
|
282
|
+
}
|
|
145
283
|
/** Lay out an indexed conversation without concatenating its historical rows on every stream frame.
|
|
146
284
|
* @param transcript - Selected session's semantic content and unfinished assistant output.
|
|
147
285
|
* @param width - Available terminal columns.
|
|
@@ -158,9 +296,32 @@ export function historyLayout(transcript, width, reasoning = 'row', overrides =
|
|
|
158
296
|
}
|
|
159
297
|
index.update(transcript.messagesForWidth(width));
|
|
160
298
|
const live = transcript.liveParts(width);
|
|
299
|
+
const keys = new Set(live.map(part => part.key));
|
|
300
|
+
for (const key of index.liveWraps.keys())
|
|
301
|
+
if (!keys.has(key))
|
|
302
|
+
index.liveWraps.delete(key);
|
|
303
|
+
for (const key of index.liveMarkdown.keys())
|
|
304
|
+
if (!keys.has(key))
|
|
305
|
+
index.liveMarkdown.delete(key);
|
|
161
306
|
const streamed = live.length ? [
|
|
162
307
|
...(transcript.liveToolOnly || index.assistantSeen ? [] : [{ text: '✦ Assistant · streaming', kind: 'assistant', bold: true }]),
|
|
163
|
-
...
|
|
308
|
+
...live.flatMap(part => {
|
|
309
|
+
if (part.kind === 'text') {
|
|
310
|
+
const previous = part.key === undefined ? undefined : index.liveMarkdown.get(part.key);
|
|
311
|
+
const extendsPrevious = previous && part.text.length >= previous.length;
|
|
312
|
+
// Include the preceding line so a split list marker or blank line can change Markdown parsing.
|
|
313
|
+
const start = extendsPrevious ? part.text.lastIndexOf('\n', Math.max(0, previous.length - 2)) + 1 : 0;
|
|
314
|
+
const rich = !!(extendsPrevious && previous.rich) || hasMarkdown(part.text.slice(start));
|
|
315
|
+
if (part.key !== undefined)
|
|
316
|
+
index.liveMarkdown.set(part.key, { length: part.text.length, rich });
|
|
317
|
+
if (rich) {
|
|
318
|
+
if (part.key !== undefined)
|
|
319
|
+
index.liveWraps.delete(part.key);
|
|
320
|
+
return partRows([part], width, liveReasoning);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return livePartRows(part, index.liveWraps, width, liveReasoning);
|
|
324
|
+
}),
|
|
164
325
|
] : [];
|
|
165
326
|
const committed = index;
|
|
166
327
|
const length = committed.length + streamed.length;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Session domain: the selected session, its transcript, layout, telemetry and navigation. */
|
|
2
|
+
export { SessionController } from './controller.ts';
|
|
3
|
+
export { contentText, toolLine, Transcript } from './transcript.ts';
|
|
4
|
+
export type { LivePhase, Message, MessagePart, ThoughtEntry } from './transcript.ts';
|
|
5
|
+
export { historyLayout, layoutStats, releaseHistoryLayout } from './history.ts';
|
|
6
|
+
export { markdownCacheStats } from './markdown.ts';
|
|
7
|
+
export type { HistoryRow, Reasoning, RowKind } from './history.ts';
|
|
8
|
+
export { Telemetry } from './telemetry.ts';
|
|
9
|
+
export type { QueuedInput } from './telemetry.ts';
|
|
10
|
+
export { DEFAULT_HISTORY_LIMITS, historyLimits } from './memory.ts';
|
|
11
|
+
export type { HistoryLimits } from './memory.ts';
|
|
12
|
+
export { navigationCommand, resolveTarget, sessionLabel } from './navigation.ts';
|
|
13
|
+
export { activeReference, fileMention, fileReferences } from './references.ts';
|
|
14
|
+
export type { FileReference } from './references.ts';
|
|
15
|
+
export { saveSessionLog } from './export.ts';
|
|
16
|
+
export type { HistorySearch, RemovalTarget } from './types.ts';
|
|
17
|
+
export type { ConnectionView } from './connection-view.ts';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Session domain: the selected session, its transcript, layout, telemetry and navigation. */
|
|
2
|
+
export { SessionController } from "./controller.js";
|
|
3
|
+
export { contentText, toolLine, Transcript } from "./transcript.js";
|
|
4
|
+
export { historyLayout, layoutStats, releaseHistoryLayout } from "./history.js";
|
|
5
|
+
export { markdownCacheStats } from "./markdown.js";
|
|
6
|
+
export { Telemetry } from "./telemetry.js";
|
|
7
|
+
export { DEFAULT_HISTORY_LIMITS, historyLimits } from "./memory.js";
|
|
8
|
+
export { navigationCommand, resolveTarget, sessionLabel } from "./navigation.js";
|
|
9
|
+
export { activeReference, fileMention, fileReferences } from "./references.js";
|
|
10
|
+
export { saveSessionLog } from "./export.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Local styles only; remote control sequences never become terminal instructions. */
|
|
2
|
+
export interface MarkdownSpan {
|
|
3
|
+
text: string;
|
|
4
|
+
bold?: boolean;
|
|
5
|
+
italic?: boolean;
|
|
6
|
+
underline?: boolean;
|
|
7
|
+
strikethrough?: boolean;
|
|
8
|
+
inverse?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** A measured row retains plain text for searching, copying and row geometry. */
|
|
11
|
+
export interface MarkdownRow {
|
|
12
|
+
text: string;
|
|
13
|
+
spans?: MarkdownSpan[];
|
|
14
|
+
}
|
|
15
|
+
/** Report the bounded math and diagram cache, so a memory sample can tell churn from retention.
|
|
16
|
+
* @returns Cached entries, their accounted characters, and lookup counters.
|
|
17
|
+
*/
|
|
18
|
+
export declare function markdownCacheStats(): {
|
|
19
|
+
entries: number;
|
|
20
|
+
chars: number;
|
|
21
|
+
hits: number;
|
|
22
|
+
misses: number;
|
|
23
|
+
};
|
|
24
|
+
/** Whether a source can contain Markdown constructs; false preserves incremental plain-text wrapping.
|
|
25
|
+
* @param source - Complete text or a delta with its preceding line.
|
|
26
|
+
* @returns Whether Markdown lexing is required.
|
|
27
|
+
*/
|
|
28
|
+
export declare function hasMarkdown(source: string): boolean;
|
|
29
|
+
/** Parse GFM and TeX into styled rows whose widths match the terminal viewport.
|
|
30
|
+
* @param source - Model or user message; remote terminal controls are removed.
|
|
31
|
+
* @param width - Available terminal columns.
|
|
32
|
+
* @returns Terminal rows, with literal source for invalid or unsupported rich blocks.
|
|
33
|
+
*/
|
|
34
|
+
export declare function markdownRows(source: string, width: number): MarkdownRow[];
|
|
35
|
+
/** Render safe, offline HTML with Mermaid SVG images and MathJax-generated MathML.
|
|
36
|
+
* @param source - Markdown source, never trusted as executable HTML.
|
|
37
|
+
* @returns An HTML fragment that requires no scripts or remote assets.
|
|
38
|
+
*/
|
|
39
|
+
export declare function markdownHtml(source: string): string;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/** Markdown becomes terminal rows before viewport indexing; source text remains in the transcript. */
|
|
2
|
+
import { Marked } from 'marked';
|
|
3
|
+
import { renderMermaidASCII, renderMermaidSVG } from 'beautiful-mermaid';
|
|
4
|
+
import stringWidth from 'string-width';
|
|
5
|
+
import wrapAnsi from 'wrap-ansi';
|
|
6
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
7
|
+
import { decodeHTML } from 'entities';
|
|
8
|
+
import { safeText } from "../transport/wire.js";
|
|
9
|
+
import { renderMath } from "./math.js";
|
|
10
|
+
const parser = new Marked({ gfm: true });
|
|
11
|
+
// Math is tokenized before Markdown escapes, but after fenced and inline code have claimed their source.
|
|
12
|
+
function mathToken(source, block) {
|
|
13
|
+
const match = block
|
|
14
|
+
? /^(?:\$\$([\s\S]+?)\$\$|\\\[([\s\S]+?)\\\])(?:[ \t]*\n|$)/.exec(source)
|
|
15
|
+
: /^(?:\$\$([^\n]+?)\$\$|\$(?![\s$])((?:\\.|[^$\n\\])+?)(?<!\s)\$(?!\d)|\\\(([^\n]+?)\\\)|\\\[([^\n]+?)\\\])/.exec(source);
|
|
16
|
+
if (!match)
|
|
17
|
+
return undefined;
|
|
18
|
+
return { type: 'math', raw: match[0], text: match.slice(1).find(value => value !== undefined),
|
|
19
|
+
display: block || source.startsWith('$$') || source.startsWith('\\[') };
|
|
20
|
+
}
|
|
21
|
+
for (const level of ['block', 'inline'])
|
|
22
|
+
parser.use({ extensions: [{
|
|
23
|
+
name: 'math', level,
|
|
24
|
+
start: source => { const index = source.search(level === 'block' ? /\$\$|\\\[/ : /\$|\\[([]/); return index < 0 ? undefined : index; },
|
|
25
|
+
tokenizer: source => mathToken(source, level === 'block'),
|
|
26
|
+
renderer: token => mathHtml(token),
|
|
27
|
+
}] });
|
|
28
|
+
const rendered = new Map();
|
|
29
|
+
let cachedChars = 0;
|
|
30
|
+
let cacheHits = 0;
|
|
31
|
+
let cacheMisses = 0;
|
|
32
|
+
function cached(key, render) {
|
|
33
|
+
const existing = rendered.get(key);
|
|
34
|
+
if (existing !== undefined) {
|
|
35
|
+
cacheHits++;
|
|
36
|
+
return existing;
|
|
37
|
+
}
|
|
38
|
+
cacheMisses++;
|
|
39
|
+
const result = render();
|
|
40
|
+
const size = key.length + result.length;
|
|
41
|
+
if (size <= 64 * 1024) {
|
|
42
|
+
while (rendered.size >= 128 || cachedChars + size > 256 * 1024) {
|
|
43
|
+
const oldest = rendered.keys().next().value;
|
|
44
|
+
cachedChars -= oldest.length + rendered.get(oldest).length;
|
|
45
|
+
rendered.delete(oldest);
|
|
46
|
+
}
|
|
47
|
+
rendered.set(key, result);
|
|
48
|
+
cachedChars += size;
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
/** Report the bounded math and diagram cache, so a memory sample can tell churn from retention.
|
|
53
|
+
* @returns Cached entries, their accounted characters, and lookup counters.
|
|
54
|
+
*/
|
|
55
|
+
export function markdownCacheStats() {
|
|
56
|
+
return { entries: rendered.size, chars: cachedChars, hits: cacheHits, misses: cacheMisses };
|
|
57
|
+
}
|
|
58
|
+
function mathText(token) {
|
|
59
|
+
return cached(`math:${token.display}:${token.text}`, () => renderMath(token.text, token.display)?.text ?? token.raw.trimEnd());
|
|
60
|
+
}
|
|
61
|
+
function mathHtml(token) {
|
|
62
|
+
return renderMath(token.text, token.display)?.html ?? `<code>${escapeHtml(token.raw)}</code>`;
|
|
63
|
+
}
|
|
64
|
+
function style(text, open, close) {
|
|
65
|
+
return `\x1b[${open}m${text.replaceAll(`\x1b[${close}m`, `\x1b[${close}m\x1b[${open}m`)}\x1b[${close}m`;
|
|
66
|
+
}
|
|
67
|
+
function plain(text) { return safeText(text).replace(/\t/g, ' '); }
|
|
68
|
+
function inline(tokens) {
|
|
69
|
+
return tokens.map(token => {
|
|
70
|
+
switch (token.type) {
|
|
71
|
+
case 'strong': return style(inline(token.tokens), 1, 22);
|
|
72
|
+
case 'em': return style(inline(token.tokens), 3, 23);
|
|
73
|
+
case 'del': return style(inline(token.tokens), 9, 29);
|
|
74
|
+
case 'codespan': return style(plain(token.text), 7, 27);
|
|
75
|
+
case 'link': {
|
|
76
|
+
const label = inline(token.tokens);
|
|
77
|
+
const href = plain(token.href);
|
|
78
|
+
return style(label, 4, 24) + (stripVTControlCharacters(label) === href ? '' : ` (${href})`);
|
|
79
|
+
}
|
|
80
|
+
case 'image': return `${inline(token.tokens)} (${plain(token.href)})`;
|
|
81
|
+
case 'br': return '\n';
|
|
82
|
+
case 'math': return mathText(token);
|
|
83
|
+
case 'html': return /^<br\s*\/?\s*>$/i.test(token.raw) ? '\n' : plain(token.raw);
|
|
84
|
+
case 'escape': return plain(token.text);
|
|
85
|
+
default: return 'tokens' in token && token.tokens ? inline(token.tokens)
|
|
86
|
+
: plain(decodeHTML('text' in token && typeof token.text === 'string' ? token.text : token.raw));
|
|
87
|
+
}
|
|
88
|
+
}).join('');
|
|
89
|
+
}
|
|
90
|
+
function wrap(text, width, trim = true) {
|
|
91
|
+
return wrapAnsi(text, Math.max(1, width), { hard: true, trim }).split('\n');
|
|
92
|
+
}
|
|
93
|
+
function table(token, width) {
|
|
94
|
+
const cells = [token.header, ...token.rows].map(row => row.map(cell => inline(cell.tokens)));
|
|
95
|
+
const count = token.header.length;
|
|
96
|
+
const available = width - 3 * count - 1;
|
|
97
|
+
// Narrow screens show each record vertically instead of squeezing columns into unreadable fragments.
|
|
98
|
+
if (available < 12 * count) {
|
|
99
|
+
if (cells.length === 1)
|
|
100
|
+
return cells[0].flatMap(cell => wrap(style(cell, 1, 22), width));
|
|
101
|
+
return cells.slice(1).flatMap((row, index) => [
|
|
102
|
+
...(index ? ['─'.repeat(Math.min(width, 20))] : []),
|
|
103
|
+
...row.flatMap((cell, column) => wrap(`${cells[0][column] ? style(cells[0][column], 1, 22) + ': ' : ''}${cell}`, width)),
|
|
104
|
+
]);
|
|
105
|
+
}
|
|
106
|
+
const widths = Array.from({ length: count }, (_, column) => Math.max(3, ...cells.map(row => Math.max(...row[column].split('\n').map(line => stringWidth(line))))));
|
|
107
|
+
while (widths.reduce((sum, value) => sum + value, 0) > available) {
|
|
108
|
+
const column = widths.indexOf(Math.max(...widths));
|
|
109
|
+
widths[column]--;
|
|
110
|
+
}
|
|
111
|
+
const border = (left, middle, right) => left + widths.map(size => '─'.repeat(size + 2)).join(middle) + right;
|
|
112
|
+
const output = [border('┌', '┬', '┐')];
|
|
113
|
+
cells.forEach((row, index) => {
|
|
114
|
+
const lines = row.map((cell, column) => wrap(index === 0 ? style(cell, 1, 22) : cell, widths[column]));
|
|
115
|
+
for (let line = 0; line < Math.max(...lines.map(cell => cell.length)); line++) {
|
|
116
|
+
output.push('│ ' + lines.map((cell, column) => {
|
|
117
|
+
const value = cell[line] ?? '';
|
|
118
|
+
const padding = widths[column] - stringWidth(value);
|
|
119
|
+
const align = token.align[column];
|
|
120
|
+
const before = align === 'right' ? padding : align === 'center' ? Math.floor(padding / 2) : 0;
|
|
121
|
+
return ' '.repeat(Math.max(0, before)) + value + ' '.repeat(Math.max(0, padding - before));
|
|
122
|
+
}).join(' │ ') + ' │');
|
|
123
|
+
}
|
|
124
|
+
if (index === 0)
|
|
125
|
+
output.push(border('├', '┼', '┤'));
|
|
126
|
+
});
|
|
127
|
+
output.push(border('└', '┴', '┘'));
|
|
128
|
+
return output;
|
|
129
|
+
}
|
|
130
|
+
function diagram(source, width) {
|
|
131
|
+
if (source.length > 16 * 1024)
|
|
132
|
+
return undefined;
|
|
133
|
+
try {
|
|
134
|
+
const text = cached(`mermaid:${source}`, () => {
|
|
135
|
+
// The ASCII renderer measures code units. Reserve one extra cell for each wide BMP character.
|
|
136
|
+
const padding = Array.from({ length: 256 }, (_, i) => String.fromCharCode(0xe000 + i)).find(c => !source.includes(c));
|
|
137
|
+
if (!padding)
|
|
138
|
+
throw new Error('No available diagram padding character');
|
|
139
|
+
const measured = [...source].map(c => c + padding.repeat(Math.max(0, stringWidth(c) - c.length))).join('');
|
|
140
|
+
return plain(renderMermaidASCII(measured, { colorMode: 'none' }).replaceAll(padding, ''));
|
|
141
|
+
});
|
|
142
|
+
const lines = text.trimEnd().split('\n').map(line => line.trimEnd());
|
|
143
|
+
return lines.every(line => stringWidth(line) <= width) ? lines : undefined;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return undefined; /* Unsupported or unfinished diagrams retain their source. */
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function blocks(tokens, width) {
|
|
150
|
+
return tokens.flatMap(token => {
|
|
151
|
+
switch (token.type) {
|
|
152
|
+
case 'space': return [''];
|
|
153
|
+
case 'def': return [];
|
|
154
|
+
case 'heading': return wrap(style(inline(token.tokens), 1, 22), width);
|
|
155
|
+
case 'paragraph':
|
|
156
|
+
case 'text': return wrap(token.tokens ? inline(token.tokens) : plain(token.text), width);
|
|
157
|
+
case 'hr': return ['─'.repeat(width)];
|
|
158
|
+
case 'table': return table(token, width);
|
|
159
|
+
case 'blockquote': return blocks(token.tokens, Math.max(1, width - 2)).map(line => width > 2 ? `│ ${line}` : line);
|
|
160
|
+
case 'list': return token.items.flatMap((item, index) => {
|
|
161
|
+
const marker = item.task ? item.checked ? '[x] ' : '[ ] ' : token.ordered ? `${Number(token.start) + index}. ` : '• ';
|
|
162
|
+
const prefix = width > marker.length + 1 ? marker : '';
|
|
163
|
+
return blocks(item.tokens.filter(child => child.type !== 'checkbox'), Math.max(1, width - prefix.length))
|
|
164
|
+
.map((line, row) => `${row === 0 ? prefix : ' '.repeat(prefix.length)}${line}`);
|
|
165
|
+
});
|
|
166
|
+
case 'code': {
|
|
167
|
+
const language = plain(token.lang ?? '').split(/\s/)[0];
|
|
168
|
+
// An open fence is source, even when its current contents form a parseable partial diagram.
|
|
169
|
+
const closed = /(?:^|\n) {0,3}(`{3,}|~{3,})[ \t]*(?:\n)?$/.test(token.raw);
|
|
170
|
+
const graph = language === 'mermaid' && closed ? diagram(token.text, width) : undefined;
|
|
171
|
+
if (graph)
|
|
172
|
+
return graph;
|
|
173
|
+
if (['math', 'latex', 'tex', 'mathjax'].includes(language) && closed) {
|
|
174
|
+
return wrap(mathText({ type: 'math', text: token.text, raw: token.text, display: true }), width, false);
|
|
175
|
+
}
|
|
176
|
+
return [...wrap(`┌ ${language || 'code'}`, width, false), ...wrap(plain(token.text), width, false), '└'];
|
|
177
|
+
}
|
|
178
|
+
case 'math': return wrap(mathText(token), width, false);
|
|
179
|
+
default: return wrap(plain(token.raw), width);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// Decode only locally generated style codes after wrap-ansi has balanced styles across line breaks.
|
|
184
|
+
function row(text) {
|
|
185
|
+
const spans = [];
|
|
186
|
+
let current = {};
|
|
187
|
+
let start = 0;
|
|
188
|
+
const properties = { 1: 'bold', 3: 'italic', 4: 'underline', 7: 'inverse', 9: 'strikethrough',
|
|
189
|
+
22: 'bold', 23: 'italic', 24: 'underline', 27: 'inverse', 29: 'strikethrough' };
|
|
190
|
+
for (const match of text.matchAll(/\x1b\[(\d+)m/g)) {
|
|
191
|
+
if (match.index > start)
|
|
192
|
+
spans.push({ text: text.slice(start, match.index), ...current });
|
|
193
|
+
const code = Number(match[1]);
|
|
194
|
+
if (code === 0)
|
|
195
|
+
current = {};
|
|
196
|
+
else if (code in properties)
|
|
197
|
+
current[properties[code]] = code < 20;
|
|
198
|
+
start = match.index + match[0].length;
|
|
199
|
+
}
|
|
200
|
+
if (start < text.length)
|
|
201
|
+
spans.push({ text: text.slice(start), ...current });
|
|
202
|
+
return { text: spans.map(span => span.text).join(''), ...(text.includes('\x1b') ? { spans } : {}) };
|
|
203
|
+
}
|
|
204
|
+
/** Whether a source can contain Markdown constructs; false preserves incremental plain-text wrapping.
|
|
205
|
+
* @param source - Complete text or a delta with its preceding line.
|
|
206
|
+
* @returns Whether Markdown lexing is required.
|
|
207
|
+
*/
|
|
208
|
+
export function hasMarkdown(source) {
|
|
209
|
+
return /[*_`~\[\]<>|$\\#&\t]|(?:^|\n)(?: {4}| {0,3}(?:[-+=]{1,}|\d+[.)])(?:\s|$))|\n\s*\n/.test(source);
|
|
210
|
+
}
|
|
211
|
+
/** Parse GFM and TeX into styled rows whose widths match the terminal viewport.
|
|
212
|
+
* @param source - Model or user message; remote terminal controls are removed.
|
|
213
|
+
* @param width - Available terminal columns.
|
|
214
|
+
* @returns Terminal rows, with literal source for invalid or unsupported rich blocks.
|
|
215
|
+
*/
|
|
216
|
+
export function markdownRows(source, width) {
|
|
217
|
+
width = Math.max(1, width);
|
|
218
|
+
if (!hasMarkdown(source))
|
|
219
|
+
return wrap(plain(source), width).map(row);
|
|
220
|
+
return blocks(parser.lexer(plain(source)), width).map(row);
|
|
221
|
+
}
|
|
222
|
+
function escapeHtml(text) {
|
|
223
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
224
|
+
}
|
|
225
|
+
function safeLink(href) {
|
|
226
|
+
return /^(?:https?:|mailto:)/i.test(href) && !/[\u0000-\u0020]/.test(href) ? escapeHtml(href) : undefined;
|
|
227
|
+
}
|
|
228
|
+
parser.use({ renderer: {
|
|
229
|
+
html: token => escapeHtml(token.text),
|
|
230
|
+
link(token) {
|
|
231
|
+
const label = this.parser.parseInline(token.tokens);
|
|
232
|
+
const href = safeLink(token.href);
|
|
233
|
+
return href ? `<a href="${href}" rel="noreferrer">${label}</a>` : `${label} (${escapeHtml(token.href)})`;
|
|
234
|
+
},
|
|
235
|
+
image: token => `${escapeHtml(token.text)} (${escapeHtml(token.href)})`,
|
|
236
|
+
code(token) {
|
|
237
|
+
const language = (token.lang ?? '').split(/\s/)[0];
|
|
238
|
+
if (language === 'mermaid' && token.text.length <= 16 * 1024) {
|
|
239
|
+
try {
|
|
240
|
+
const svg = renderMermaidSVG(token.text);
|
|
241
|
+
// SVG image documents cannot run script or load external resources in the exported page.
|
|
242
|
+
return `<img alt="Mermaid diagram" src="data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}">`;
|
|
243
|
+
}
|
|
244
|
+
catch { /* Unsupported Mermaid syntax remains readable in a code block. */ }
|
|
245
|
+
}
|
|
246
|
+
if (['math', 'latex', 'tex', 'mathjax'].includes(language ?? ''))
|
|
247
|
+
return mathHtml({ type: 'math', text: token.text, raw: token.text, display: true });
|
|
248
|
+
return `<pre><code>${escapeHtml(token.text)}</code></pre>`;
|
|
249
|
+
},
|
|
250
|
+
} });
|
|
251
|
+
/** Render safe, offline HTML with Mermaid SVG images and MathJax-generated MathML.
|
|
252
|
+
* @param source - Markdown source, never trusted as executable HTML.
|
|
253
|
+
* @returns An HTML fragment that requires no scripts or remote assets.
|
|
254
|
+
*/
|
|
255
|
+
export function markdownHtml(source) { return parser.parse(safeText(source), { async: false }); }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import '@mathjax/src/js/input/tex/base/BaseConfiguration.js';
|
|
2
|
+
import '@mathjax/src/js/input/tex/ams/AmsConfiguration.js';
|
|
3
|
+
/** Convert one TeX expression, without extension autoloads, network access or shared macro state.
|
|
4
|
+
* @param source - TeX without delimiters.
|
|
5
|
+
* @param display - Block rather than inline mathematics.
|
|
6
|
+
* @returns Unicode approximation and MathJax-generated MathML, or undefined for invalid TeX.
|
|
7
|
+
*/
|
|
8
|
+
export declare function renderMath(source: string, display: boolean): {
|
|
9
|
+
text: string;
|
|
10
|
+
html: string;
|
|
11
|
+
} | undefined;
|