@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.
Files changed (129) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +54 -25
  3. package/README.zh.md +54 -25
  4. package/dist/catalog/controller.d.ts +32 -0
  5. package/dist/catalog/controller.js +88 -0
  6. package/dist/catalog/index.d.ts +2 -0
  7. package/dist/catalog/index.js +2 -0
  8. package/dist/{cli.js → cli/index.js} +40 -24
  9. package/dist/controller/connection.d.ts +80 -0
  10. package/dist/controller/connection.js +190 -0
  11. package/dist/controller/controller.d.ts +260 -0
  12. package/dist/controller/controller.js +362 -0
  13. package/dist/controller/index.d.ts +5 -0
  14. package/dist/controller/index.js +3 -0
  15. package/dist/controller/memory-log.d.ts +35 -0
  16. package/dist/controller/memory-log.js +95 -0
  17. package/dist/cost/controller.d.ts +34 -0
  18. package/dist/cost/controller.js +107 -0
  19. package/dist/cost/index.d.ts +9 -0
  20. package/dist/cost/index.js +7 -0
  21. package/dist/cost/ledger-files.d.ts +20 -0
  22. package/dist/cost/ledger-files.js +128 -0
  23. package/dist/cost/ledger.d.ts +65 -0
  24. package/dist/cost/ledger.js +136 -0
  25. package/dist/cost/pricing.d.ts +48 -0
  26. package/dist/cost/pricing.js +141 -0
  27. package/dist/cost/records.d.ts +17 -0
  28. package/dist/cost/records.js +66 -0
  29. package/dist/cost/scanner.d.ts +22 -0
  30. package/dist/cost/scanner.js +95 -0
  31. package/dist/cost/types.d.ts +69 -0
  32. package/dist/cost/types.js +3 -0
  33. package/dist/index.d.ts +3 -0
  34. package/dist/index.js +2 -0
  35. package/dist/session/connection-view.d.ts +18 -0
  36. package/dist/session/connection-view.js +1 -0
  37. package/dist/{controller.d.ts → session/controller.d.ts} +119 -137
  38. package/dist/session/controller.js +616 -0
  39. package/dist/session/export-html.d.ts +9 -0
  40. package/dist/session/export-html.js +39 -0
  41. package/dist/session/export.d.ts +9 -0
  42. package/dist/session/export.js +38 -0
  43. package/dist/{history.d.ts → session/history.d.ts} +18 -1
  44. package/dist/session/history.js +338 -0
  45. package/dist/session/index.d.ts +17 -0
  46. package/dist/session/index.js +10 -0
  47. package/dist/session/markdown.d.ts +39 -0
  48. package/dist/session/markdown.js +255 -0
  49. package/dist/session/math.d.ts +11 -0
  50. package/dist/session/math.js +82 -0
  51. package/dist/{navigation.d.ts → session/navigation.d.ts} +1 -1
  52. package/dist/{navigation.js → session/navigation.js} +1 -1
  53. package/dist/{references.js → session/references.js} +1 -1
  54. package/dist/{telemetry.d.ts → session/telemetry.d.ts} +12 -1
  55. package/dist/{telemetry.js → session/telemetry.js} +18 -4
  56. package/dist/{transcript.d.ts → session/transcript.d.ts} +23 -1
  57. package/dist/{transcript.js → session/transcript.js} +36 -18
  58. package/dist/session/types.d.ts +18 -0
  59. package/dist/session/types.js +2 -0
  60. package/dist/state.d.ts +41 -0
  61. package/dist/state.js +9 -0
  62. package/dist/storage/directories.d.ts +14 -0
  63. package/dist/storage/directories.js +24 -0
  64. package/dist/storage/files.d.ts +43 -0
  65. package/dist/storage/files.js +135 -0
  66. package/dist/storage/index.d.ts +3 -0
  67. package/dist/storage/index.js +3 -0
  68. package/dist/transport/auth.js +65 -0
  69. package/dist/{client.d.ts → transport/client.d.ts} +8 -1
  70. package/dist/{client.js → transport/client.js} +21 -4
  71. package/dist/transport/host.d.ts +13 -0
  72. package/dist/transport/host.js +1 -0
  73. package/dist/ui/app.d.ts +11 -0
  74. package/dist/ui/app.js +726 -0
  75. package/dist/ui/chat/header.d.ts +11 -0
  76. package/dist/ui/chat/header.js +14 -0
  77. package/dist/{history-view.d.ts → ui/chat/history-view.d.ts} +1 -1
  78. package/dist/{history-view.js → ui/chat/history-view.js} +3 -3
  79. package/dist/ui/chat/status.d.ts +79 -0
  80. package/dist/ui/chat/status.js +311 -0
  81. package/dist/ui/chat/viewport.d.ts +17 -0
  82. package/dist/ui/chat/viewport.js +14 -0
  83. package/dist/ui/commands/parse.d.ts +96 -0
  84. package/dist/ui/commands/parse.js +121 -0
  85. package/dist/ui/commands/registry.d.ts +33 -0
  86. package/dist/ui/commands/registry.js +72 -0
  87. package/dist/ui/copy-mode.d.ts +4 -0
  88. package/dist/ui/copy-mode.js +6 -0
  89. package/dist/{cost-view.d.ts → ui/dialogs/cost.d.ts} +1 -1
  90. package/dist/{cost-view.js → ui/dialogs/cost.js} +3 -3
  91. package/dist/ui/dialogs/index.d.ts +120 -0
  92. package/dist/ui/dialogs/index.js +113 -0
  93. package/dist/ui/dialogs/picker.d.ts +18 -0
  94. package/dist/ui/dialogs/picker.js +38 -0
  95. package/dist/ui/frozen.d.ts +8 -0
  96. package/dist/ui/frozen.js +7 -0
  97. package/dist/ui/input/references.d.ts +12 -0
  98. package/dist/ui/input/references.js +15 -0
  99. package/dist/ui/mount.d.ts +6 -0
  100. package/dist/ui/mount.js +11 -0
  101. package/dist/{theme.d.ts → ui/theme/index.d.ts} +1 -1
  102. package/dsht-m.png +0 -0
  103. package/package.json +20 -14
  104. package/dist/app.d.ts +0 -21
  105. package/dist/app.js +0 -746
  106. package/dist/auth.js +0 -108
  107. package/dist/controller.js +0 -923
  108. package/dist/cost.d.ts +0 -119
  109. package/dist/cost.js +0 -313
  110. package/dist/history.js +0 -177
  111. package/dist/status.d.ts +0 -28
  112. package/dist/status.js +0 -157
  113. package/dsht.png +0 -0
  114. /package/dist/{cli.d.ts → cli/index.d.ts} +0 -0
  115. /package/dist/{memory.d.ts → session/memory.d.ts} +0 -0
  116. /package/dist/{memory.js → session/memory.js} +0 -0
  117. /package/dist/{references.d.ts → session/references.d.ts} +0 -0
  118. /package/dist/{auth.d.ts → transport/auth.d.ts} +0 -0
  119. /package/dist/{endpoint.d.ts → transport/endpoint.d.ts} +0 -0
  120. /package/dist/{endpoint.js → transport/endpoint.js} +0 -0
  121. /package/dist/{wire.d.ts → transport/wire.d.ts} +0 -0
  122. /package/dist/{wire.js → transport/wire.js} +0 -0
  123. /package/dist/{input-history.d.ts → ui/input/history.d.ts} +0 -0
  124. /package/dist/{input-history.js → ui/input/history.js} +0 -0
  125. /package/dist/{input.d.ts → ui/input/input.d.ts} +0 -0
  126. /package/dist/{input.js → ui/input/input.js} +0 -0
  127. /package/dist/{mouse.d.ts → ui/input/mouse.d.ts} +0 -0
  128. /package/dist/{mouse.js → ui/input/mouse.js} +0 -0
  129. /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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
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 './wire.ts';
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 "./wire.js";
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 "./wire.js";
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 './wire.ts';
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 "./wire.js";
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, array(items).length);
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, array(frame.items).length);
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 './wire.ts';
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 "./wire.js";
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.blocks.clear();
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.blocks.clear();
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.blocks.clear();
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.blocks.clear();
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.blocks.clear();
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.blocks.clear();
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.blocks.clear();
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
- this.blocks.set(index, { type: 'tool-call', id: string(chunk.id),
591
- name: typeof chunk.name === 'string' ? chunk.name : block?.name ?? '',
592
- arguments: '' });
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));