@itookit/dsht 0.2.4 → 0.3.2
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 +54 -25
- package/README.zh.md +54 -25
- 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} +40 -24
- package/dist/controller/connection.d.ts +80 -0
- package/dist/controller/connection.js +190 -0
- package/dist/controller/controller.d.ts +260 -0
- package/dist/controller/controller.js +362 -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/controller.d.ts +34 -0
- package/dist/cost/controller.js +107 -0
- package/dist/cost/index.d.ts +9 -0
- package/dist/cost/index.js +7 -0
- package/dist/cost/ledger-files.d.ts +20 -0
- package/dist/cost/ledger-files.js +128 -0
- package/dist/cost/ledger.d.ts +65 -0
- package/dist/cost/ledger.js +136 -0
- package/dist/cost/pricing.d.ts +48 -0
- package/dist/cost/pricing.js +141 -0
- package/dist/cost/records.d.ts +17 -0
- package/dist/cost/records.js +66 -0
- package/dist/cost/scanner.d.ts +22 -0
- package/dist/cost/scanner.js +95 -0
- package/dist/cost/types.d.ts +69 -0
- package/dist/cost/types.js +3 -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} +119 -137
- 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/session/export.d.ts +9 -0
- package/dist/session/export.js +38 -0
- package/dist/{history.d.ts → session/history.d.ts} +18 -1
- package/dist/session/history.js +338 -0
- 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/{navigation.d.ts → session/navigation.d.ts} +1 -1
- package/dist/{navigation.js → session/navigation.js} +1 -1
- package/dist/{references.js → session/references.js} +1 -1
- package/dist/{telemetry.d.ts → session/telemetry.d.ts} +12 -1
- package/dist/{telemetry.js → session/telemetry.js} +18 -4
- package/dist/{transcript.d.ts → session/transcript.d.ts} +23 -1
- package/dist/{transcript.js → session/transcript.js} +36 -18
- 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 +43 -0
- package/dist/storage/files.js +135 -0
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.js +3 -0
- package/dist/transport/auth.js +65 -0
- package/dist/{client.d.ts → transport/client.d.ts} +8 -1
- package/dist/{client.js → transport/client.js} +21 -4
- 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 +726 -0
- package/dist/ui/chat/header.d.ts +11 -0
- package/dist/ui/chat/header.js +14 -0
- package/dist/{history-view.d.ts → ui/chat/history-view.d.ts} +1 -1
- package/dist/{history-view.js → ui/chat/history-view.js} +3 -3
- package/dist/ui/chat/status.d.ts +79 -0
- package/dist/ui/chat/status.js +311 -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 +96 -0
- package/dist/ui/commands/parse.js +121 -0
- package/dist/ui/commands/registry.d.ts +33 -0
- package/dist/ui/commands/registry.js +72 -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} +3 -3
- 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/dsht-m.png +0 -0
- package/package.json +20 -14
- package/dist/app.d.ts +0 -21
- package/dist/app.js +0 -746
- package/dist/auth.js +0 -108
- package/dist/controller.js +0 -923
- package/dist/cost.d.ts +0 -119
- package/dist/cost.js +0 -313
- package/dist/history.js +0 -177
- package/dist/status.d.ts +0 -28
- package/dist/status.js +0 -157
- package/dsht.png +0 -0
- /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/{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
|
@@ -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;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** MathJax compiles TeX locally; terminal output keeps explicit grouping when Unicode cannot express it. */
|
|
2
|
+
import { mathjax } from '@mathjax/src/js/mathjax.js';
|
|
3
|
+
import { TeX } from '@mathjax/src/js/input/tex.js';
|
|
4
|
+
import { liteAdaptor } from '@mathjax/src/js/adaptors/liteAdaptor.js';
|
|
5
|
+
import { RegisterHTMLHandler } from '@mathjax/src/js/handlers/html.js';
|
|
6
|
+
import { STATE } from '@mathjax/src/js/core/MathItem.js';
|
|
7
|
+
import { TextNode } from '@mathjax/src/js/core/MmlTree/MmlNode.js';
|
|
8
|
+
import { SerializedMmlVisitor } from '@mathjax/src/js/core/MmlTree/SerializedMmlVisitor.js';
|
|
9
|
+
import '@mathjax/src/js/input/tex/base/BaseConfiguration.js';
|
|
10
|
+
import '@mathjax/src/js/input/tex/ams/AmsConfiguration.js';
|
|
11
|
+
import { safeText } from "../transport/wire.js";
|
|
12
|
+
RegisterHTMLHandler(liteAdaptor());
|
|
13
|
+
const superscripts = Object.fromEntries([...('0123456789+-=()ni')].map((c, i) => [c, [...'⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾ⁿⁱ'][i]]));
|
|
14
|
+
const subscripts = Object.fromEntries([...'0123456789+-=()aehijklmnoprstuvx'].map((c, i) => [c, [...'₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓ'][i]]));
|
|
15
|
+
function script(text, sub) {
|
|
16
|
+
const alphabet = sub ? subscripts : superscripts;
|
|
17
|
+
return [...text].every(c => alphabet[c]) ? [...text].map(c => alphabet[c]).join('') : `${sub ? '_' : '^'}(${text})`;
|
|
18
|
+
}
|
|
19
|
+
function readable(node) {
|
|
20
|
+
if (node instanceof TextNode)
|
|
21
|
+
return node.getText();
|
|
22
|
+
const children = node.childNodes.map(readable);
|
|
23
|
+
const [a = '', b = '', c = ''] = children;
|
|
24
|
+
let base = node.childNodes[0];
|
|
25
|
+
while (base && ['TeXAtom', 'mrow', 'inferredMrow', 'mstyle'].includes(base.kind) && base.childNodes.length === 1)
|
|
26
|
+
base = base.childNodes[0];
|
|
27
|
+
const grouped = base && !base.isToken && base.childNodes.length > 1 ? `(${a})` : a;
|
|
28
|
+
switch (node.kind) {
|
|
29
|
+
case 'mfrac': return `(${a})/(${b})`;
|
|
30
|
+
case 'msqrt': return `√(${children.join('')})`;
|
|
31
|
+
case 'mroot': return `root(${b}, ${a})`;
|
|
32
|
+
case 'msup': return `${grouped}${script(b, false)}`;
|
|
33
|
+
case 'msub': return `${grouped}${script(b, true)}`;
|
|
34
|
+
case 'msubsup': return `${grouped}${script(b, true)}${script(c, false)}`;
|
|
35
|
+
case 'munder': return `${a}_(${b})`;
|
|
36
|
+
case 'mover': return `${a}^(${b})`;
|
|
37
|
+
case 'munderover': return `${a}_(${b})^(${c})`;
|
|
38
|
+
case 'mtable': return `[${children.join('; ')}]`;
|
|
39
|
+
case 'mtr':
|
|
40
|
+
case 'mlabeledtr': return children.join(', ');
|
|
41
|
+
case 'mspace': return ' ';
|
|
42
|
+
case 'mphantom': return '';
|
|
43
|
+
case 'math':
|
|
44
|
+
case 'mrow':
|
|
45
|
+
case 'inferredMrow':
|
|
46
|
+
case 'TeXAtom':
|
|
47
|
+
case 'mstyle':
|
|
48
|
+
case 'mi':
|
|
49
|
+
case 'mn':
|
|
50
|
+
case 'mo':
|
|
51
|
+
case 'mtext':
|
|
52
|
+
case 'mtd': return children.join('');
|
|
53
|
+
default: throw new Error(`Unsupported terminal math node: ${node.kind}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Convert one TeX expression, without extension autoloads, network access or shared macro state.
|
|
57
|
+
* @param source - TeX without delimiters.
|
|
58
|
+
* @param display - Block rather than inline mathematics.
|
|
59
|
+
* @returns Unicode approximation and MathJax-generated MathML, or undefined for invalid TeX.
|
|
60
|
+
*/
|
|
61
|
+
export function renderMath(source, display) {
|
|
62
|
+
if (source.length > 16 * 1024)
|
|
63
|
+
return undefined;
|
|
64
|
+
try {
|
|
65
|
+
const tex = new TeX({ packages: ['base', 'ams'], maxBuffer: 16 * 1024,
|
|
66
|
+
formatError(_jax, error) { throw error; } });
|
|
67
|
+
const document = mathjax.document('', { InputJax: tex });
|
|
68
|
+
const node = document.convert(source, { display, end: STATE.CONVERT });
|
|
69
|
+
const html = new SerializedMmlVisitor().visitTree(node);
|
|
70
|
+
let text;
|
|
71
|
+
try {
|
|
72
|
+
text = readable(node);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
text = source; /* Valid MathML with no faithful terminal notation retains its TeX. */
|
|
76
|
+
}
|
|
77
|
+
return { text: safeText(text), html };
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return undefined; /* Incomplete or invalid model-authored TeX remains visible as source. */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Shared display names and unambiguous slash-command target resolution. */
|
|
2
|
-
import { type ObjectValue } from '
|
|
2
|
+
import { type ObjectValue } from '../transport/wire.ts';
|
|
3
3
|
/** Parse workspace and resume navigation, including their long aliases. */
|
|
4
4
|
export declare function navigationCommand(value: string): {
|
|
5
5
|
kind: 'workspace' | 'session';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Shared display names and unambiguous slash-command target resolution. */
|
|
2
|
-
import { safeText, string } from "
|
|
2
|
+
import { safeText, string } from "../transport/wire.js";
|
|
3
3
|
/** Parse workspace and resume navigation, including their long aliases. */
|
|
4
4
|
export function navigationCommand(value) {
|
|
5
5
|
const match = /^\/(ws|workspace|workspaces|resume|session|sessions)(?:\s+(.+))?$/.exec(value);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Independent adapter for Harness path-only @ references; no file bytes are read. */
|
|
2
|
-
import { array, object, string } from "
|
|
2
|
+
import { array, object, string } from "../transport/wire.js";
|
|
3
3
|
/** Find the unfinished reference at the end of the draft; email addresses do not trigger it.
|
|
4
4
|
* @param text - Complete composer draft.
|
|
5
5
|
* @returns The replaceable token and host query, or undefined outside a reference.
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/** Host projection values with per-key watermarks; snapshots never roll back newer updates. */
|
|
2
|
-
import { type ObjectValue } from '
|
|
2
|
+
import { type ObjectValue } from '../transport/wire.ts';
|
|
3
|
+
/** One host-owned pending input, removable only while its occurrence remains queued. */
|
|
4
|
+
export interface QueuedInput {
|
|
5
|
+
id: string;
|
|
6
|
+
placement: 'queued' | 'steering' | 'context';
|
|
7
|
+
text: string;
|
|
8
|
+
}
|
|
3
9
|
/** Generation-local projection, inbox and background-job data for every session. */
|
|
4
10
|
export declare class Telemetry {
|
|
5
11
|
private readonly retainedKeys?;
|
|
@@ -27,5 +33,10 @@ export declare class Telemetry {
|
|
|
27
33
|
queued?: number;
|
|
28
34
|
jobs?: number;
|
|
29
35
|
};
|
|
36
|
+
/** Read the selected session's authoritative pending inputs; absence means no observed queue.
|
|
37
|
+
* @param id - Session identity.
|
|
38
|
+
* @returns Pending inputs in host order.
|
|
39
|
+
*/
|
|
40
|
+
pending(id?: string): readonly QueuedInput[];
|
|
30
41
|
private entry;
|
|
31
42
|
}
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
/** Host projection values with per-key watermarks; snapshots never roll back newer updates. */
|
|
2
|
-
import { array, object, string } from "
|
|
2
|
+
import { array, object, string } from "../transport/wire.js";
|
|
3
|
+
function queuedInputs(value) {
|
|
4
|
+
return array(value).map(value => {
|
|
5
|
+
const item = object(value);
|
|
6
|
+
if (!['queued', 'steering', 'context'].includes(string(item.placement)))
|
|
7
|
+
throw new Error('Invalid queue placement');
|
|
8
|
+
return { id: string(item.id), placement: item.placement,
|
|
9
|
+
text: array(object(item.message).content).map(object).map(block => block.type === 'text' ? string(block.text) : `[${string(block.type)}]`).join(' ') };
|
|
10
|
+
});
|
|
11
|
+
}
|
|
3
12
|
/** Generation-local projection, inbox and background-job data for every session. */
|
|
4
13
|
export class Telemetry {
|
|
5
14
|
retainedKeys;
|
|
@@ -24,7 +33,7 @@ export class Telemetry {
|
|
|
24
33
|
for (const [id, projection] of Object.entries(object(baseline.projections)))
|
|
25
34
|
this.snapshot(id, projection);
|
|
26
35
|
for (const [id, items] of Object.entries(object(baseline.queues)))
|
|
27
|
-
this.queues.set(id,
|
|
36
|
+
this.queues.set(id, queuedInputs(items));
|
|
28
37
|
for (const [id, items] of Object.entries(object(baseline.jobs)))
|
|
29
38
|
this.jobs.set(id, activeJobs(items));
|
|
30
39
|
this.ready = true;
|
|
@@ -47,7 +56,7 @@ export class Telemetry {
|
|
|
47
56
|
entry.revisions.set(key, seq);
|
|
48
57
|
}
|
|
49
58
|
else if (frame.type === 'queue')
|
|
50
|
-
this.queues.set(id,
|
|
59
|
+
this.queues.set(id, queuedInputs(frame.items));
|
|
51
60
|
else if (frame.type === 'jobs')
|
|
52
61
|
this.jobs.set(id, activeJobs(frame.items));
|
|
53
62
|
else
|
|
@@ -85,8 +94,13 @@ export class Telemetry {
|
|
|
85
94
|
*/
|
|
86
95
|
view(id) {
|
|
87
96
|
return { values: id ? this.entries.get(id)?.values ?? {} : {},
|
|
88
|
-
queued: id ? this.queues.get(id) : undefined, jobs: id ? this.jobs.get(id) : undefined };
|
|
97
|
+
queued: id ? this.queues.get(id)?.length : undefined, jobs: id ? this.jobs.get(id) : undefined };
|
|
89
98
|
}
|
|
99
|
+
/** Read the selected session's authoritative pending inputs; absence means no observed queue.
|
|
100
|
+
* @param id - Session identity.
|
|
101
|
+
* @returns Pending inputs in host order.
|
|
102
|
+
*/
|
|
103
|
+
pending(id) { return id ? this.queues.get(id) ?? [] : []; }
|
|
90
104
|
entry(id) {
|
|
91
105
|
let entry = this.entries.get(id);
|
|
92
106
|
if (!entry) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HistoryLimits } from './memory.ts';
|
|
2
|
-
import { type Json } from '
|
|
2
|
+
import { type Json } from '../transport/wire.ts';
|
|
3
3
|
interface ToolSummary {
|
|
4
4
|
name: string;
|
|
5
5
|
operation?: string;
|
|
@@ -25,6 +25,17 @@ export interface MessagePart {
|
|
|
25
25
|
text: string;
|
|
26
26
|
/** Closed live reasoning can fold before the assistant message is committed. */
|
|
27
27
|
closed?: boolean;
|
|
28
|
+
/** Stable identity of a live part, so its rows can be wrapped incrementally as it grows. */
|
|
29
|
+
key?: string;
|
|
30
|
+
}
|
|
31
|
+
/** The stripe of live work the unfinished attempt is in. */
|
|
32
|
+
export interface LivePhase {
|
|
33
|
+
/** Which kind of delta last moved the attempt forward. */
|
|
34
|
+
kind: 'thinking' | 'tool' | 'output';
|
|
35
|
+
/** Tool name when `kind` is `tool`, so the bar can name what is running. */
|
|
36
|
+
name?: string;
|
|
37
|
+
/** Epoch milliseconds when this phase began. */
|
|
38
|
+
startedAt: number;
|
|
28
39
|
}
|
|
29
40
|
/** A displayed message retains the durable sequence for stable reconciliation. */
|
|
30
41
|
export interface Message {
|
|
@@ -61,6 +72,7 @@ export declare class Transcript {
|
|
|
61
72
|
private oldestSeq;
|
|
62
73
|
private turnMarker;
|
|
63
74
|
private attempt;
|
|
75
|
+
private phase;
|
|
64
76
|
private nextIndex;
|
|
65
77
|
private revision;
|
|
66
78
|
private legacyStream;
|
|
@@ -97,6 +109,12 @@ export declare class Transcript {
|
|
|
97
109
|
private dropProjections;
|
|
98
110
|
private storeEvent;
|
|
99
111
|
private deleteEvent;
|
|
112
|
+
/** The stripe of work the live attempt is in: the phase of the block that last took a delta.
|
|
113
|
+
*
|
|
114
|
+
* Its age answers "what is it doing, and for how long" without inferring anything from silence.
|
|
115
|
+
* @returns Phase kind, the tool name when the phase is a tool call, and when the phase began.
|
|
116
|
+
*/
|
|
117
|
+
get livePhase(): LivePhase | undefined;
|
|
100
118
|
/** Start of the open durable turn, when its timestamp is present in the retained window. */
|
|
101
119
|
get activeTurnStartedAt(): number | undefined;
|
|
102
120
|
/** Cached prompt/reasoning summaries, oldest first; stream deltas never invalidate this index. */
|
|
@@ -144,6 +162,10 @@ export declare class Transcript {
|
|
|
144
162
|
*/
|
|
145
163
|
private foldLegacy;
|
|
146
164
|
private validatePackedTiming;
|
|
165
|
+
/** Drop the live attempt's blocks and the phase derived from them. */
|
|
166
|
+
private clearBlocks;
|
|
167
|
+
/** Record the phase a delta moved the attempt into; the status bar shows its age. */
|
|
168
|
+
private notePhase;
|
|
147
169
|
private chunk;
|
|
148
170
|
}
|
|
149
171
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Human transcript projection from durable events and ephemeral assistant chunks. */
|
|
2
2
|
import sliceAnsi from 'slice-ansi';
|
|
3
|
-
import { array, object, safeText, string } from "
|
|
3
|
+
import { array, object, safeText, string } from "../transport/wire.js";
|
|
4
4
|
function toolSummary(block) {
|
|
5
5
|
let args = {};
|
|
6
6
|
if (typeof block.arguments === 'string' && block.arguments.trimStart().startsWith('{')) {
|
|
@@ -77,6 +77,7 @@ export class Transcript {
|
|
|
77
77
|
oldestSeq;
|
|
78
78
|
turnMarker;
|
|
79
79
|
attempt;
|
|
80
|
+
phase;
|
|
80
81
|
nextIndex = 0;
|
|
81
82
|
revision = 0;
|
|
82
83
|
legacyStream = false;
|
|
@@ -114,7 +115,7 @@ export class Transcript {
|
|
|
114
115
|
this.closedBlocks.clear();
|
|
115
116
|
this.oldestSeq = undefined;
|
|
116
117
|
this.turnMarker = undefined;
|
|
117
|
-
this.
|
|
118
|
+
this.clearBlocks();
|
|
118
119
|
this.keysDirty = true;
|
|
119
120
|
this.projectionRevision++;
|
|
120
121
|
this.legacyDirty = true;
|
|
@@ -157,8 +158,7 @@ export class Transcript {
|
|
|
157
158
|
throw new Error('Assistant stream revision gap');
|
|
158
159
|
this.revision = number(live.revision);
|
|
159
160
|
if (live.type === 'start') {
|
|
160
|
-
this.
|
|
161
|
-
this.closedBlocks.clear();
|
|
161
|
+
this.clearBlocks();
|
|
162
162
|
this.attempt = string(live.attemptId);
|
|
163
163
|
this.nextIndex = 0;
|
|
164
164
|
}
|
|
@@ -172,7 +172,7 @@ export class Transcript {
|
|
|
172
172
|
}
|
|
173
173
|
else if (live.type === 'end') {
|
|
174
174
|
this.attempt = undefined;
|
|
175
|
-
this.
|
|
175
|
+
this.clearBlocks();
|
|
176
176
|
}
|
|
177
177
|
else
|
|
178
178
|
throw new Error('Unknown assistant stream frame');
|
|
@@ -257,8 +257,7 @@ export class Transcript {
|
|
|
257
257
|
this.events.clear();
|
|
258
258
|
this.sizes.clear();
|
|
259
259
|
this.storedBytes = 0;
|
|
260
|
-
this.
|
|
261
|
-
this.closedBlocks.clear();
|
|
260
|
+
this.clearBlocks();
|
|
262
261
|
this.transientSeqs.clear();
|
|
263
262
|
this.sortedSeqs = [];
|
|
264
263
|
this.displayed = [];
|
|
@@ -296,6 +295,17 @@ export class Transcript {
|
|
|
296
295
|
this.sizes.delete(seq);
|
|
297
296
|
this.events.delete(seq);
|
|
298
297
|
}
|
|
298
|
+
/** The stripe of work the live attempt is in: the phase of the block that last took a delta.
|
|
299
|
+
*
|
|
300
|
+
* Its age answers "what is it doing, and for how long" without inferring anything from silence.
|
|
301
|
+
* @returns Phase kind, the tool name when the phase is a tool call, and when the phase began.
|
|
302
|
+
*/
|
|
303
|
+
get livePhase() {
|
|
304
|
+
if (this.phase === undefined || this.blocks.size === 0)
|
|
305
|
+
return undefined;
|
|
306
|
+
const { kind, name, startedAt } = this.phase;
|
|
307
|
+
return { kind, ...(name === undefined ? {} : { name }), startedAt };
|
|
308
|
+
}
|
|
299
309
|
/** Start of the open durable turn, when its timestamp is present in the retained window. */
|
|
300
310
|
get activeTurnStartedAt() {
|
|
301
311
|
return this.turnMarker?.start;
|
|
@@ -449,6 +459,7 @@ export class Transcript {
|
|
|
449
459
|
* @returns Ordered blocks without terminal style escapes.
|
|
450
460
|
*/
|
|
451
461
|
liveParts(width) {
|
|
462
|
+
const attempt = this.liveAttemptKey ?? 'live';
|
|
452
463
|
return [...this.blocks].sort(([a], [b]) => a - b).map(([index, block]) => {
|
|
453
464
|
const closed = this.closedBlocks.has(index);
|
|
454
465
|
const cached = this.liveProjection.get(block);
|
|
@@ -457,6 +468,7 @@ export class Transcript {
|
|
|
457
468
|
const part = {
|
|
458
469
|
kind: block.type === 'reasoning' ? 'reasoning' : block.type === 'tool-call' ? 'tool' : 'text',
|
|
459
470
|
text: cached?.width === width ? cached.part.text : contentText([block], undefined, width), closed,
|
|
471
|
+
key: `${attempt}:${index}`,
|
|
460
472
|
};
|
|
461
473
|
this.liveProjection.set(block, { width, part });
|
|
462
474
|
return part;
|
|
@@ -520,8 +532,7 @@ export class Transcript {
|
|
|
520
532
|
foldLegacy(added) {
|
|
521
533
|
let events;
|
|
522
534
|
if (this.legacyDirty) {
|
|
523
|
-
this.
|
|
524
|
-
this.closedBlocks.clear();
|
|
535
|
+
this.clearBlocks();
|
|
525
536
|
this.legacyPosition = '';
|
|
526
537
|
this.legacySeq = -1;
|
|
527
538
|
this.legacyDirty = false;
|
|
@@ -533,17 +544,14 @@ export class Transcript {
|
|
|
533
544
|
for (const event of events) {
|
|
534
545
|
this.legacySeq = Math.max(this.legacySeq, number(event.seq));
|
|
535
546
|
if (['step/start', 'step/end', 'turn/end', 'assistant/message', 'assistant/attempt'].includes(string(event.type))) {
|
|
536
|
-
this.
|
|
537
|
-
this.closedBlocks.clear();
|
|
547
|
+
this.clearBlocks();
|
|
538
548
|
this.legacyPosition = '';
|
|
539
549
|
}
|
|
540
550
|
else if (event.type === 'assistant/chunk' || string(event.type).startsWith('chunkrow/')) {
|
|
541
551
|
const data = object(event.data);
|
|
542
552
|
const nextPosition = `${number(data.turn)}:${number(data.step)}`;
|
|
543
|
-
if (this.legacyPosition !== nextPosition)
|
|
544
|
-
this.
|
|
545
|
-
this.closedBlocks.clear();
|
|
546
|
-
}
|
|
553
|
+
if (this.legacyPosition !== nextPosition)
|
|
554
|
+
this.clearBlocks();
|
|
547
555
|
this.legacyPosition = nextPosition;
|
|
548
556
|
switch (event.type) {
|
|
549
557
|
case 'assistant/chunk':
|
|
@@ -572,6 +580,15 @@ export class Transcript {
|
|
|
572
580
|
if (count === 0 || array(data.dt).length !== count - 1)
|
|
573
581
|
throw new Error('Invalid packed history member count');
|
|
574
582
|
}
|
|
583
|
+
/** Drop the live attempt's blocks and the phase derived from them. */
|
|
584
|
+
clearBlocks() { this.blocks.clear(); this.closedBlocks.clear(); this.phase = undefined; }
|
|
585
|
+
/** Record the phase a delta moved the attempt into; the status bar shows its age. */
|
|
586
|
+
notePhase(kind, name) {
|
|
587
|
+
const key = kind === 'tool' ? `tool:${name ?? ''}` : kind;
|
|
588
|
+
if (this.phase?.key === key)
|
|
589
|
+
return;
|
|
590
|
+
this.phase = { key, kind, ...(name === undefined ? {} : { name }), startedAt: Date.now() };
|
|
591
|
+
}
|
|
575
592
|
chunk(chunk) {
|
|
576
593
|
if (chunk.type === 'text-delta' || chunk.type === 'tool-call-delta') {
|
|
577
594
|
for (const [index, block] of this.blocks)
|
|
@@ -583,13 +600,14 @@ export class Transcript {
|
|
|
583
600
|
const block = this.blocks.get(index);
|
|
584
601
|
this.blocks.set(index, { type: chunk.type === 'text-delta' ? 'text' : 'reasoning',
|
|
585
602
|
text: (block ? string(block.text) : '') + string(chunk.text) });
|
|
603
|
+
this.notePhase(chunk.type === 'text-delta' ? 'output' : 'thinking');
|
|
586
604
|
}
|
|
587
605
|
else if (chunk.type === 'tool-call-delta') {
|
|
588
606
|
const index = number(chunk.index);
|
|
589
607
|
const block = this.blocks.get(index);
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
608
|
+
const name = typeof chunk.name === 'string' ? chunk.name : typeof block?.name === 'string' ? block.name : '';
|
|
609
|
+
this.blocks.set(index, { type: 'tool-call', id: string(chunk.id), name, arguments: '' });
|
|
610
|
+
this.notePhase('tool', name === '' ? undefined : name);
|
|
593
611
|
}
|
|
594
612
|
else if (chunk.type === 'block-end') {
|
|
595
613
|
this.blocks.set(number(chunk.index), object(chunk.block));
|