pi-tau-web-server 1.0.8

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 (98) hide show
  1. package/README.md +303 -0
  2. package/bin/auth.js +96 -0
  3. package/bin/config.js +99 -0
  4. package/bin/model-utils.js +134 -0
  5. package/bin/server-main.js +1189 -0
  6. package/bin/sessions.js +492 -0
  7. package/bin/tau.js +8 -0
  8. package/bin/tree.js +248 -0
  9. package/bin/types.js +2 -0
  10. package/package.json +74 -0
  11. package/public/app-main.js +2025 -0
  12. package/public/app-types.js +1 -0
  13. package/public/app.js +1 -0
  14. package/public/command-palette.js +42 -0
  15. package/public/dialogs.js +199 -0
  16. package/public/file-browser.js +196 -0
  17. package/public/icons/apple-touch-icon.png +0 -0
  18. package/public/icons/favicon-16.png +0 -0
  19. package/public/icons/favicon-32.png +0 -0
  20. package/public/icons/tau-192.png +0 -0
  21. package/public/icons/tau-512.png +0 -0
  22. package/public/icons/tau-logo.png +0 -0
  23. package/public/icons/tau-logo.svg +13 -0
  24. package/public/icons/tau-maskable-512.png +0 -0
  25. package/public/icons/tau-new.png +0 -0
  26. package/public/index.html +264 -0
  27. package/public/launcher-panel.js +54 -0
  28. package/public/launcher.js +84 -0
  29. package/public/manifest.json +28 -0
  30. package/public/markdown.js +336 -0
  31. package/public/message-renderer.js +268 -0
  32. package/public/model-picker.js +478 -0
  33. package/public/session-sidebar.js +460 -0
  34. package/public/session-stats-card.js +123 -0
  35. package/public/state.js +81 -0
  36. package/public/style.css +3864 -0
  37. package/public/sw.js +66 -0
  38. package/public/themes.js +72 -0
  39. package/public/tool-card.js +317 -0
  40. package/public/tree-view.js +474 -0
  41. package/public/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  42. package/public/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  43. package/public/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  44. package/public/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  45. package/public/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  46. package/public/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  47. package/public/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  48. package/public/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  49. package/public/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  50. package/public/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  51. package/public/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  52. package/public/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  53. package/public/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  54. package/public/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  55. package/public/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  56. package/public/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  57. package/public/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  58. package/public/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  59. package/public/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  60. package/public/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  61. package/public/vendor/katex/katex.min.css +1 -0
  62. package/public/vendor/katex/katex.min.js +1 -0
  63. package/public/voice-input.js +74 -0
  64. package/public/websocket-client.js +156 -0
  65. package/scripts/copy-katex.mjs +32 -0
  66. package/src/pi-extension/tau-tree.ts +71 -0
  67. package/src/public/app-main.ts +2190 -0
  68. package/src/public/app-types.ts +96 -0
  69. package/src/public/app.ts +1 -0
  70. package/src/public/command-palette.ts +53 -0
  71. package/src/public/dialogs.ts +251 -0
  72. package/src/public/file-browser.ts +224 -0
  73. package/src/public/launcher-panel.ts +68 -0
  74. package/src/public/launcher.ts +101 -0
  75. package/src/public/legacy-dom.d.ts +29 -0
  76. package/src/public/markdown.ts +372 -0
  77. package/src/public/message-renderer.ts +311 -0
  78. package/src/public/model-picker.ts +500 -0
  79. package/src/public/session-sidebar.ts +522 -0
  80. package/src/public/session-stats-card.ts +176 -0
  81. package/src/public/state.ts +96 -0
  82. package/src/public/sw.ts +79 -0
  83. package/src/public/themes.ts +73 -0
  84. package/src/public/tool-card.ts +375 -0
  85. package/src/public/tree-view.ts +527 -0
  86. package/src/public/voice-input.ts +98 -0
  87. package/src/public/websocket-client.ts +165 -0
  88. package/src/server/auth.ts +88 -0
  89. package/src/server/config.ts +88 -0
  90. package/src/server/model-utils.ts +122 -0
  91. package/src/server/server-main.ts +1004 -0
  92. package/src/server/sessions.ts +481 -0
  93. package/src/server/tau.ts +9 -0
  94. package/src/server/tree.ts +288 -0
  95. package/src/server/types.ts +68 -0
  96. package/tsconfig.json +3 -0
  97. package/tsconfig.public.json +15 -0
  98. package/tsconfig.server.json +16 -0
@@ -0,0 +1,474 @@
1
+ const HINT_DEFAULT = '↑↓ move · Enter jump · Esc close';
2
+ const HINT_STREAMING = 'Session is streaming — navigation is disabled until the turn finishes.';
3
+ // customType of the internal marker entries the server appends to persist a
4
+ // leaf move (keep in sync with NAVIGATION_MARKER_TYPE in src/server/tree.ts).
5
+ // They are tau plumbing, not conversation content, so the tree never shows
6
+ // them — not even in "All entries" mode.
7
+ const NAVIGATION_MARKER_TYPE = 'tau:navigate-tree';
8
+ // Indent guides are capped so deeply-branched trees stay readable on narrow
9
+ // screens (depth counts branch points, not entries — see flatten()).
10
+ const MAX_GUIDE_DEPTH = 6;
11
+ // Small inline-SVG role glyphs, matching the app's existing icon style
12
+ // (16px, stroke currentColor).
13
+ const SVG_ATTRS = 'width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
14
+ const ICONS = {
15
+ user: `<svg ${SVG_ATTRS}><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`,
16
+ assistant: `<svg ${SVG_ATTRS}><path d="M12 3l2 5.5L19.5 10 14 12l-2 5.5L10 12l-5.5-2L10 8.5z"/></svg>`,
17
+ tool: `<svg ${SVG_ATTRS}><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>`,
18
+ compaction: `<svg ${SVG_ATTRS}><rect x="2" y="3" width="20" height="5" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><line x1="10" y1="12" x2="14" y2="12"/></svg>`,
19
+ meta: `<svg ${SVG_ATTRS}><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`,
20
+ };
21
+ function firstLine(text) {
22
+ return (String(text || '').split('\n').find((line) => line.trim()) || '').trim();
23
+ }
24
+ function messageText(msg) {
25
+ if (!msg)
26
+ return '';
27
+ if (typeof msg.content === 'string')
28
+ return msg.content;
29
+ if (Array.isArray(msg.content)) {
30
+ return msg.content.filter((b) => b.type === 'text').map((b) => b.text || '').join('\n');
31
+ }
32
+ return '';
33
+ }
34
+ // Classify an entry for glyph + filtering, and produce the one-line snippet.
35
+ function describeEntry(entry) {
36
+ if (entry.type === 'message') {
37
+ const role = entry.message?.role || '';
38
+ if (role === 'user')
39
+ return { kind: 'user', text: firstLine(messageText(entry.message)) || '(empty message)' };
40
+ if (role === 'assistant') {
41
+ const text = firstLine(messageText(entry.message));
42
+ if (text)
43
+ return { kind: 'assistant', text };
44
+ const toolNames = Array.isArray(entry.message?.content)
45
+ ? entry.message.content.filter((b) => b.type === 'toolCall').map((b) => b.name || 'tool').join(', ')
46
+ : '';
47
+ return { kind: 'assistant', text: toolNames ? `→ ${toolNames}` : '(empty message)' };
48
+ }
49
+ if (role === 'toolResult')
50
+ return { kind: 'tool', text: entry.message?.toolName || 'tool result' };
51
+ return { kind: 'meta', text: firstLine(messageText(entry.message)) || role || 'message' };
52
+ }
53
+ if (entry.type === 'custom_message') {
54
+ return { kind: 'user', text: firstLine(String(entry.content || '')) || entry.customType || 'custom message' };
55
+ }
56
+ if (entry.type === 'custom')
57
+ return { kind: 'meta', text: entry.customType || 'custom' };
58
+ if (entry.type === 'compaction') {
59
+ const summary = firstLine(String(entry.summary || ''));
60
+ return { kind: 'compaction', text: summary ? `Compaction — ${summary}` : 'Compaction' };
61
+ }
62
+ if (entry.type === 'branch_summary') {
63
+ const summary = firstLine(String(entry.summary || ''));
64
+ return { kind: 'compaction', text: summary ? `Branch summary — ${summary}` : 'Branch summary' };
65
+ }
66
+ // Model id can contain slashes; display is always the provider/id join.
67
+ if (entry.type === 'model_change')
68
+ return { kind: 'meta', text: `model → ${[entry.provider, entry.modelId].filter(Boolean).join('/')}` };
69
+ if (entry.type === 'thinking_level_change')
70
+ return { kind: 'meta', text: `thinking → ${entry.thinkingLevel || ''}` };
71
+ if (entry.type === 'session_info')
72
+ return { kind: 'meta', text: `name → ${entry.name || ''}` };
73
+ if (entry.type === 'label')
74
+ return { kind: 'meta', text: `label → ${entry.label || ''}` };
75
+ return { kind: 'meta', text: entry.type || 'entry' };
76
+ }
77
+ export function setupTreeView(options) {
78
+ const { getActiveLiveSessionId, isStreaming, setComposerText, flashStatusError } = options;
79
+ // Static index.html shell elements — assert non-null at the query site,
80
+ // matching the other setup modules.
81
+ const treeBtn = document.getElementById('tree-btn');
82
+ const overlay = document.getElementById('tree-view-overlay');
83
+ const modal = document.getElementById('tree-view');
84
+ const listEl = document.getElementById('tree-view-list');
85
+ const messageEl = document.getElementById('tree-view-message');
86
+ const filterBtn = document.getElementById('tree-view-filter');
87
+ const closeBtn = document.getElementById('tree-view-close');
88
+ let treeData = [];
89
+ let leafId = null;
90
+ let showAll = false; // default: messages only (pi's default /tree filter)
91
+ let rows = [];
92
+ let rowEls = [];
93
+ let activeIndex = -1;
94
+ let openSessionId = null;
95
+ let navigating = false;
96
+ function isOpen() {
97
+ return !modal.classList.contains('hidden');
98
+ }
99
+ function setMessage(text, isError = false) {
100
+ messageEl.textContent = text;
101
+ messageEl.classList.toggle('error', isError);
102
+ }
103
+ function updateFilterButton() {
104
+ filterBtn.textContent = showAll ? 'All entries' : 'Messages only';
105
+ filterBtn.setAttribute('aria-pressed', showAll ? 'true' : 'false');
106
+ filterBtn.title = showAll ? 'Showing every entry — click to hide tool results and housekeeping entries' : 'Hiding tool results and housekeeping entries — click to show every entry';
107
+ }
108
+ // Flatten the tree depth-first. Depth counts BRANCH POINTS, not entries: a
109
+ // linear run of parent→child entries stays at one indent level and only a
110
+ // node with several children pushes its children one level deeper —
111
+ // otherwise every ordinary exchange would gain a level and long sessions
112
+ // would indent themselves off-screen. Filtered-out entries never render:
113
+ // their children are promoted into their place (at the depth the hidden
114
+ // entry would have had), so the branch structure stays readable in
115
+ // messages-only mode. "Messages only" mirrors pi's no-tools filter: tool
116
+ // results and housekeeping entries (model/thinking/name changes, custom
117
+ // meta entries) are hidden, messages and compactions stay. tau's internal
118
+ // navigation markers are plumbing and are hidden in every mode.
119
+ function flatten(nodes, depth, out) {
120
+ for (const node of nodes) {
121
+ const entry = node.entry;
122
+ const isInternalMarker = entry.type === 'custom' && entry.customType === NAVIGATION_MARKER_TYPE;
123
+ const kind = describeEntry(entry).kind;
124
+ const hidden = isInternalMarker || (!showAll && (kind === 'tool' || kind === 'meta'));
125
+ if (!hidden)
126
+ out.push({ node, depth });
127
+ const childDepth = depth + ((node.children || []).length > 1 ? 1 : 0);
128
+ flatten(node.children, childDepth, out);
129
+ }
130
+ return out;
131
+ }
132
+ // ids on the active path (root → current leaf), derived by walking
133
+ // parentId links from the leaf.
134
+ function computeActivePath() {
135
+ const byId = new Map();
136
+ const walk = (nodes) => {
137
+ for (const node of nodes) {
138
+ if (node.entry?.id)
139
+ byId.set(node.entry.id, node.entry);
140
+ walk(node.children || []);
141
+ }
142
+ };
143
+ walk(treeData);
144
+ const path = new Set();
145
+ let cursor = leafId;
146
+ while (cursor && byId.has(cursor) && !path.has(cursor)) {
147
+ path.add(cursor);
148
+ cursor = byId.get(cursor)?.parentId || null;
149
+ }
150
+ return path;
151
+ }
152
+ function setActiveIndex(index, scroll = false) {
153
+ if (!rows.length) {
154
+ activeIndex = -1;
155
+ listEl.removeAttribute('aria-activedescendant');
156
+ return;
157
+ }
158
+ activeIndex = Math.max(0, Math.min(index, rows.length - 1));
159
+ rowEls.forEach((el, i) => {
160
+ const active = i === activeIndex;
161
+ el.classList.toggle('active', active);
162
+ el.setAttribute('aria-selected', active ? 'true' : 'false');
163
+ });
164
+ // Keep the roving highlight visible to screen readers: DOM focus stays on
165
+ // the listbox, aria-activedescendant follows the highlighted option.
166
+ const activeEl = rowEls[activeIndex];
167
+ if (activeEl?.id)
168
+ listEl.setAttribute('aria-activedescendant', activeEl.id);
169
+ if (scroll)
170
+ activeEl?.scrollIntoView({ block: 'nearest' });
171
+ }
172
+ function render(scrollLeafIntoView = false) {
173
+ rows = flatten(treeData, 0, []);
174
+ rowEls = [];
175
+ listEl.innerHTML = '';
176
+ listEl.removeAttribute('aria-activedescendant');
177
+ const path = computeActivePath();
178
+ const streaming = isStreaming();
179
+ setMessage(streaming ? HINT_STREAMING : HINT_DEFAULT, false);
180
+ listEl.classList.toggle('tree-view-inert', streaming);
181
+ if (!rows.length) {
182
+ const empty = document.createElement('div');
183
+ empty.className = 'tree-view-empty';
184
+ empty.textContent = 'No entries in this session yet.';
185
+ listEl.appendChild(empty);
186
+ activeIndex = -1;
187
+ return;
188
+ }
189
+ // The "current" marker goes on the DEEPEST VISIBLE row of the active
190
+ // path, not on the raw leaf entry: the leaf is often hidden (tau's
191
+ // navigation marker, a filtered tool result) or a housekeeping entry
192
+ // appended below the spot the user actually jumped to. DFS order lists
193
+ // ancestors before descendants, so the last on-path row is the deepest.
194
+ let currentIndex = -1;
195
+ rows.forEach((row, index) => {
196
+ if (row.node.entry.id && path.has(row.node.entry.id))
197
+ currentIndex = index;
198
+ });
199
+ rows.forEach((row, index) => {
200
+ const entry = row.node.entry;
201
+ const { kind, text } = describeEntry(entry);
202
+ const onPath = !!entry.id && path.has(entry.id);
203
+ const isCurrent = index === currentIndex;
204
+ const btn = document.createElement('button');
205
+ btn.type = 'button';
206
+ btn.id = `tree-row-${index}`;
207
+ btn.className = `tree-row${onPath ? ' on-path' : ''}${isCurrent ? ' leaf' : ''}`;
208
+ btn.setAttribute('role', 'option');
209
+ btn.setAttribute('aria-selected', 'false');
210
+ if (streaming)
211
+ btn.disabled = true;
212
+ btn.title = text;
213
+ const guides = document.createElement('span');
214
+ guides.className = 'tree-guides';
215
+ for (let i = 0; i < Math.min(row.depth, MAX_GUIDE_DEPTH); i++) {
216
+ const guide = document.createElement('span');
217
+ guide.className = 'tree-guide';
218
+ guides.appendChild(guide);
219
+ }
220
+ btn.appendChild(guides);
221
+ const icon = document.createElement('span');
222
+ icon.className = `tree-row-icon tree-row-icon-${kind}`;
223
+ icon.innerHTML = ICONS[kind] || ICONS.meta;
224
+ btn.appendChild(icon);
225
+ const snippet = document.createElement('span');
226
+ snippet.className = 'tree-row-text';
227
+ snippet.textContent = text;
228
+ btn.appendChild(snippet);
229
+ const label = row.node.label;
230
+ if (label) {
231
+ const badge = document.createElement('span');
232
+ badge.className = 'tree-row-label';
233
+ badge.textContent = label;
234
+ btn.appendChild(badge);
235
+ }
236
+ if (isCurrent) {
237
+ const current = document.createElement('span');
238
+ current.className = 'tree-row-current';
239
+ current.textContent = 'current';
240
+ btn.appendChild(current);
241
+ }
242
+ btn.addEventListener('mouseenter', () => setActiveIndex(index));
243
+ btn.addEventListener('click', () => { void selectRow(index); });
244
+ listEl.appendChild(btn);
245
+ rowEls.push(btn);
246
+ });
247
+ setActiveIndex(currentIndex >= 0 ? currentIndex : rows.length - 1);
248
+ if (scrollLeafIntoView) {
249
+ requestAnimationFrame(() => rowEls[activeIndex]?.scrollIntoView({ block: 'center' }));
250
+ }
251
+ }
252
+ async function selectRow(index) {
253
+ const row = rows[index];
254
+ const entryId = row?.node.entry?.id;
255
+ if (!entryId || navigating)
256
+ return;
257
+ if (isStreaming()) {
258
+ setMessage(HINT_STREAMING, false);
259
+ return;
260
+ }
261
+ const sessionId = openSessionId || getActiveLiveSessionId();
262
+ if (!sessionId) {
263
+ setMessage('Select a live Tau tab first.', true);
264
+ return;
265
+ }
266
+ navigating = true;
267
+ setMessage('Jumping…', false);
268
+ try {
269
+ const resp = await fetch('/api/rpc', {
270
+ method: 'POST',
271
+ headers: { 'Content-Type': 'application/json' },
272
+ body: JSON.stringify({ type: 'navigate_tree', sessionId, entryId }),
273
+ });
274
+ const data = await resp.json();
275
+ if (data?.success) {
276
+ const editorText = data.data?.editorText;
277
+ close();
278
+ // The conversation re-renders when the server broadcasts a fresh
279
+ // snapshot; we only need to hand the message text to the composer.
280
+ if (typeof editorText === 'string')
281
+ setComposerText(editorText);
282
+ }
283
+ else {
284
+ const error = data?.error || 'Failed to navigate the session tree';
285
+ setMessage(error, true);
286
+ flashStatusError(error);
287
+ }
288
+ }
289
+ catch {
290
+ const error = 'Failed to navigate the session tree';
291
+ setMessage(error, true);
292
+ flashStatusError(error);
293
+ }
294
+ finally {
295
+ navigating = false;
296
+ }
297
+ }
298
+ // Page step for PageUp/PageDown, mirroring pi's TUI tree paging.
299
+ const PAGE_STEP = 10;
300
+ function onKeyDown(e) {
301
+ if (!isOpen())
302
+ return;
303
+ if (e.key === 'Escape') {
304
+ e.preventDefault();
305
+ e.stopPropagation();
306
+ close();
307
+ return;
308
+ }
309
+ if (!rows.length)
310
+ return;
311
+ // Arrow keys wrap around like the model picker's list.
312
+ if (e.key === 'ArrowDown') {
313
+ e.preventDefault();
314
+ setActiveIndex((activeIndex + 1) % rows.length, true);
315
+ return;
316
+ }
317
+ if (e.key === 'ArrowUp') {
318
+ e.preventDefault();
319
+ setActiveIndex((activeIndex - 1 + rows.length) % rows.length, true);
320
+ return;
321
+ }
322
+ if (e.key === 'Home') {
323
+ e.preventDefault();
324
+ setActiveIndex(0, true);
325
+ return;
326
+ }
327
+ if (e.key === 'End') {
328
+ e.preventDefault();
329
+ setActiveIndex(rows.length - 1, true);
330
+ return;
331
+ }
332
+ if (e.key === 'PageDown') {
333
+ e.preventDefault();
334
+ setActiveIndex(activeIndex + PAGE_STEP, true);
335
+ return;
336
+ }
337
+ if (e.key === 'PageUp') {
338
+ e.preventDefault();
339
+ setActiveIndex(activeIndex - PAGE_STEP, true);
340
+ return;
341
+ }
342
+ if (e.key === 'Enter') {
343
+ e.preventDefault();
344
+ if (activeIndex >= 0)
345
+ void selectRow(activeIndex);
346
+ }
347
+ }
348
+ // Monotonic sequence so a slow get_tree response never clobbers a newer one
349
+ // (open → turn-end reload → snapshot reload can overlap).
350
+ let loadSeq = 0;
351
+ async function loadTree(centerCurrent) {
352
+ const sessionId = openSessionId;
353
+ if (!sessionId)
354
+ return;
355
+ const seq = ++loadSeq;
356
+ try {
357
+ const resp = await fetch('/api/rpc', {
358
+ method: 'POST',
359
+ headers: { 'Content-Type': 'application/json' },
360
+ body: JSON.stringify({ type: 'get_tree', sessionId }),
361
+ });
362
+ const data = await resp.json();
363
+ if (!isOpen() || openSessionId !== sessionId || seq !== loadSeq)
364
+ return;
365
+ if (!data?.success || !data.data) {
366
+ setMessage(data?.error || 'Failed to load the session tree', true);
367
+ return;
368
+ }
369
+ treeData = (data.data.tree || []);
370
+ leafId = data.data.leafId ?? null;
371
+ render(centerCurrent);
372
+ }
373
+ catch {
374
+ if (isOpen() && openSessionId === sessionId && seq === loadSeq) {
375
+ setMessage('Failed to load the session tree', true);
376
+ }
377
+ }
378
+ }
379
+ async function open() {
380
+ const sessionId = getActiveLiveSessionId();
381
+ if (!sessionId) {
382
+ flashStatusError('Select a live Tau tab first.');
383
+ return;
384
+ }
385
+ openSessionId = sessionId;
386
+ treeData = [];
387
+ leafId = null;
388
+ rows = [];
389
+ rowEls = [];
390
+ activeIndex = -1;
391
+ listEl.innerHTML = '';
392
+ listEl.classList.remove('tree-view-inert');
393
+ updateFilterButton();
394
+ setMessage('Loading session tree…', false);
395
+ modal.classList.remove('hidden');
396
+ overlay.classList.remove('hidden');
397
+ document.addEventListener('keydown', onKeyDown, true);
398
+ // Move focus into the dialog (the model picker focuses its input the same
399
+ // way) so Tab does not keep walking the page behind the overlay and the
400
+ // listbox's aria-activedescendant highlight is announced.
401
+ listEl.focus();
402
+ await loadTree(true);
403
+ }
404
+ function close() {
405
+ const hadFocus = modal.contains(document.activeElement);
406
+ modal.classList.add('hidden');
407
+ overlay.classList.add('hidden');
408
+ document.removeEventListener('keydown', onKeyDown, true);
409
+ listEl.innerHTML = '';
410
+ listEl.removeAttribute('aria-activedescendant');
411
+ treeData = [];
412
+ rows = [];
413
+ rowEls = [];
414
+ activeIndex = -1;
415
+ openSessionId = null;
416
+ // Hand focus back to the button that opened the dialog.
417
+ if (hadFocus && treeBtn instanceof HTMLElement && !treeBtn.classList.contains('hidden'))
418
+ treeBtn.focus();
419
+ }
420
+ // Keep an open modal in sync with the session's live state instead of
421
+ // freezing whatever was true at open time. Streaming starting disables the
422
+ // rows in place; streaming ending means the turn appended entries, so the
423
+ // tree is refetched (which also re-enables the rows and re-centers on the
424
+ // new current position).
425
+ function notifyStreamingChanged(sessionId, streaming) {
426
+ if (!isOpen() || !openSessionId || (sessionId && sessionId !== openSessionId))
427
+ return;
428
+ if (streaming) {
429
+ listEl.classList.add('tree-view-inert');
430
+ rowEls.forEach((el) => { el.disabled = true; });
431
+ if (!navigating)
432
+ setMessage(HINT_STREAMING, false);
433
+ }
434
+ else {
435
+ void loadTree(true);
436
+ }
437
+ }
438
+ // Another client moved this session's leaf (the server broadcast a fresh
439
+ // snapshot) — refetch so the displayed tree is not stale.
440
+ function notifyTreeChanged(sessionId) {
441
+ if (!isOpen() || !openSessionId || (sessionId && sessionId !== openSessionId))
442
+ return;
443
+ if (!navigating)
444
+ void loadTree(true);
445
+ }
446
+ function closeIfOpen() {
447
+ if (!isOpen())
448
+ return false;
449
+ close();
450
+ return true;
451
+ }
452
+ // Header entry button visibility is gated on having an active live
453
+ // session, mirroring the other session-dependent header controls.
454
+ function setVisible(visible) {
455
+ treeBtn.classList.toggle('hidden', !visible);
456
+ }
457
+ filterBtn.addEventListener('click', () => {
458
+ showAll = !showAll;
459
+ updateFilterButton();
460
+ // Re-render with centering, like open(): rebuilding the list resets the
461
+ // scroll position, so without it toggling the filter would throw the view
462
+ // back to the session root and lose the current position.
463
+ if (treeData.length || leafId)
464
+ render(true);
465
+ });
466
+ treeBtn.addEventListener('click', () => { void open(); });
467
+ overlay.addEventListener('click', close);
468
+ closeBtn.addEventListener('click', close);
469
+ // Focusable so open() can move focus into the dialog; keyboard interaction
470
+ // is roving-highlight (aria-activedescendant), not per-row tab stops.
471
+ listEl.tabIndex = -1;
472
+ updateFilterButton();
473
+ return { open, close, closeIfOpen, setVisible, notifyStreamingChanged, notifyTreeChanged };
474
+ }
@@ -0,0 +1 @@
1
+ @font-face{font-display:block;font-family:KaTeX_AMS;font-style:normal;font-weight:400;src:url(fonts/KaTeX_AMS-Regular.woff2) format("woff2"),url(fonts/KaTeX_AMS-Regular.woff) format("woff"),url(fonts/KaTeX_AMS-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Caligraphic;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Caligraphic-Bold.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Bold.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Bold.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Caligraphic;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Caligraphic-Regular.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Regular.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Fraktur;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Fraktur-Bold.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Bold.woff) format("woff"),url(fonts/KaTeX_Fraktur-Bold.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Fraktur;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Fraktur-Regular.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Regular.woff) format("woff"),url(fonts/KaTeX_Fraktur-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Main-Bold.woff2) format("woff2"),url(fonts/KaTeX_Main-Bold.woff) format("woff"),url(fonts/KaTeX_Main-Bold.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:italic;font-weight:700;src:url(fonts/KaTeX_Main-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Main-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Main-BoldItalic.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:italic;font-weight:400;src:url(fonts/KaTeX_Main-Italic.woff2) format("woff2"),url(fonts/KaTeX_Main-Italic.woff) format("woff"),url(fonts/KaTeX_Main-Italic.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Main-Regular.woff2) format("woff2"),url(fonts/KaTeX_Main-Regular.woff) format("woff"),url(fonts/KaTeX_Main-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Math;font-style:italic;font-weight:700;src:url(fonts/KaTeX_Math-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Math-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Math-BoldItalic.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Math;font-style:italic;font-weight:400;src:url(fonts/KaTeX_Math-Italic.woff2) format("woff2"),url(fonts/KaTeX_Math-Italic.woff) format("woff"),url(fonts/KaTeX_Math-Italic.ttf) format("truetype")}@font-face{font-display:block;font-family:"KaTeX_SansSerif";font-style:normal;font-weight:700;src:url(fonts/KaTeX_SansSerif-Bold.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Bold.woff) format("woff"),url(fonts/KaTeX_SansSerif-Bold.ttf) format("truetype")}@font-face{font-display:block;font-family:"KaTeX_SansSerif";font-style:italic;font-weight:400;src:url(fonts/KaTeX_SansSerif-Italic.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Italic.woff) format("woff"),url(fonts/KaTeX_SansSerif-Italic.ttf) format("truetype")}@font-face{font-display:block;font-family:"KaTeX_SansSerif";font-style:normal;font-weight:400;src:url(fonts/KaTeX_SansSerif-Regular.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Regular.woff) format("woff"),url(fonts/KaTeX_SansSerif-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Script;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Script-Regular.woff2) format("woff2"),url(fonts/KaTeX_Script-Regular.woff) format("woff"),url(fonts/KaTeX_Script-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size1;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size1-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size1-Regular.woff) format("woff"),url(fonts/KaTeX_Size1-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size2;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size2-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size2-Regular.woff) format("woff"),url(fonts/KaTeX_Size2-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size3;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size3-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size3-Regular.woff) format("woff"),url(fonts/KaTeX_Size3-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size4;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size4-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size4-Regular.woff) format("woff"),url(fonts/KaTeX_Size4-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Typewriter;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Typewriter-Regular.woff2) format("woff2"),url(fonts/KaTeX_Typewriter-Regular.woff) format("woff"),url(fonts/KaTeX_Typewriter-Regular.ttf) format("truetype")}.katex{font:normal 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;position:relative;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important;border-color:currentColor}.katex .katex-version:after{content:"0.18.0"}.katex .katex-mathml{border:0;-webkit-clip-path:inset(50%);clip-path:inset(50%);height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.katex .katex-html>.katex-newline{display:block}.katex .katex-base{position:relative;white-space:nowrap;width:-webkit-min-content;width:-moz-min-content;width:min-content}.katex .katex-base,.katex .katex-strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathnormal{font-family:KaTeX_Math;font-style:italic}.katex .mathit{font-family:KaTeX_Main;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-style:italic;font-weight:700}.katex .amsrm,.katex .mathbb,.katex .textbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak,.katex .textfrak{font-family:KaTeX_Fraktur}.katex .mathboldfrak,.katex .textboldfrak{font-family:KaTeX_Fraktur;font-weight:700}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr,.katex .textscr{font-family:KaTeX_Script}.katex .mathsf,.katex .textsf{font-family:KaTeX_SansSerif}.katex .mathboldsf,.katex .textboldsf{font-family:KaTeX_SansSerif;font-weight:700}.katex .mathitsf,.katex .mathsfit,.katex .textitsf{font-family:KaTeX_SansSerif;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{border-collapse:collapse;display:inline-table;table-layout:fixed}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;position:relative;vertical-align:bottom}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}.katex .vlist-t2{margin-right:-2px}.katex .vlist-s{display:table-cell;font-size:1px;min-width:2px;vertical-align:bottom;width:2px}.katex .katex-vbox{align-items:baseline;display:inline-flex;flex-direction:column}.katex .katex-thinbox{display:inline-flex;flex-direction:row;max-width:0;width:0}.katex .msupsub{text-align:left}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .katex-hdashline,.katex .katex-hline,.katex .katex-overline .overline-line,.katex .katex-rule,.katex .katex-underline .underline-line,.katex .mfrac .frac-line{min-height:1px}.katex .mspace{display:inline-block}.katex .katex-smash{display:inline;line-height:0}.katex .clap,.katex .llap,.katex .rlap{position:relative;width:0}.katex .clap>.katex-inner,.katex .llap>.katex-inner,.katex .rlap>.katex-inner{position:absolute}.katex .clap>.katex-fix,.katex .llap>.katex-fix,.katex .rlap>.katex-fix{display:inline-block}.katex .llap>.katex-inner{right:0}.katex .clap>.katex-inner,.katex .rlap>.katex-inner{left:0}.katex .clap>.katex-inner>span{margin-left:-50%;margin-right:50%}.katex .katex-rule{border:0 solid;display:inline-block;position:relative}.katex .katex-hline,.katex .katex-overline .overline-line,.katex .katex-underline .underline-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .katex-hdashline{border-bottom-style:dashed;display:inline-block;width:100%}.katex .sqrt>.katex-root{margin-left:.2777777778em;margin-right:-.5555555556em}.katex .fontsize-ensurer.reset-size1.size1,.katex .katex-sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .katex-sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .katex-sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .katex-sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .katex-sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .katex-sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .katex-sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .katex-sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .katex-sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .katex-sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .katex-sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .katex-sizing.reset-size2.size1{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .katex-sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .katex-sizing.reset-size2.size3{font-size:1.1666666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .katex-sizing.reset-size2.size4{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .katex-sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .katex-sizing.reset-size2.size6{font-size:1.6666666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .katex-sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .katex-sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .katex-sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .katex-sizing.reset-size2.size10{font-size:3.4566666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .katex-sizing.reset-size2.size11{font-size:4.1466666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .katex-sizing.reset-size3.size1{font-size:.7142857143em}.katex .fontsize-ensurer.reset-size3.size2,.katex .katex-sizing.reset-size3.size2{font-size:.8571428571em}.katex .fontsize-ensurer.reset-size3.size3,.katex .katex-sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .katex-sizing.reset-size3.size4{font-size:1.1428571429em}.katex .fontsize-ensurer.reset-size3.size5,.katex .katex-sizing.reset-size3.size5{font-size:1.2857142857em}.katex .fontsize-ensurer.reset-size3.size6,.katex .katex-sizing.reset-size3.size6{font-size:1.4285714286em}.katex .fontsize-ensurer.reset-size3.size7,.katex .katex-sizing.reset-size3.size7{font-size:1.7142857143em}.katex .fontsize-ensurer.reset-size3.size8,.katex .katex-sizing.reset-size3.size8{font-size:2.0571428571em}.katex .fontsize-ensurer.reset-size3.size9,.katex .katex-sizing.reset-size3.size9{font-size:2.4685714286em}.katex .fontsize-ensurer.reset-size3.size10,.katex .katex-sizing.reset-size3.size10{font-size:2.9628571429em}.katex .fontsize-ensurer.reset-size3.size11,.katex .katex-sizing.reset-size3.size11{font-size:3.5542857143em}.katex .fontsize-ensurer.reset-size4.size1,.katex .katex-sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .katex-sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .katex-sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .katex-sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .katex-sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .katex-sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .katex-sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .katex-sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .katex-sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .katex-sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .katex-sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .katex-sizing.reset-size5.size1{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .katex-sizing.reset-size5.size2{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .katex-sizing.reset-size5.size3{font-size:.7777777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .katex-sizing.reset-size5.size4{font-size:.8888888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .katex-sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .katex-sizing.reset-size5.size6{font-size:1.1111111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .katex-sizing.reset-size5.size7{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .katex-sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .katex-sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .katex-sizing.reset-size5.size10{font-size:2.3044444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .katex-sizing.reset-size5.size11{font-size:2.7644444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .katex-sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .katex-sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .katex-sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .katex-sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .katex-sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .katex-sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .katex-sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .katex-sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .katex-sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .katex-sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .katex-sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .katex-sizing.reset-size7.size1{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .katex-sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .katex-sizing.reset-size7.size3{font-size:.5833333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .katex-sizing.reset-size7.size4{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .katex-sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .katex-sizing.reset-size7.size6{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .katex-sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .katex-sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .katex-sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .katex-sizing.reset-size7.size10{font-size:1.7283333333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .katex-sizing.reset-size7.size11{font-size:2.0733333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .katex-sizing.reset-size8.size1{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .katex-sizing.reset-size8.size2{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .katex-sizing.reset-size8.size3{font-size:.4861111111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .katex-sizing.reset-size8.size4{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .katex-sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .katex-sizing.reset-size8.size6{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .katex-sizing.reset-size8.size7{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .katex-sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .katex-sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .katex-sizing.reset-size8.size10{font-size:1.4402777778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .katex-sizing.reset-size8.size11{font-size:1.7277777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .katex-sizing.reset-size9.size1{font-size:.2893518519em}.katex .fontsize-ensurer.reset-size9.size2,.katex .katex-sizing.reset-size9.size2{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .katex-sizing.reset-size9.size3{font-size:.4050925926em}.katex .fontsize-ensurer.reset-size9.size4,.katex .katex-sizing.reset-size9.size4{font-size:.462962963em}.katex .fontsize-ensurer.reset-size9.size5,.katex .katex-sizing.reset-size9.size5{font-size:.5208333333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .katex-sizing.reset-size9.size6{font-size:.5787037037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .katex-sizing.reset-size9.size7{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .katex-sizing.reset-size9.size8{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .katex-sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .katex-sizing.reset-size9.size10{font-size:1.2002314815em}.katex .fontsize-ensurer.reset-size9.size11,.katex .katex-sizing.reset-size9.size11{font-size:1.4398148148em}.katex .fontsize-ensurer.reset-size10.size1,.katex .katex-sizing.reset-size10.size1{font-size:.2410800386em}.katex .fontsize-ensurer.reset-size10.size2,.katex .katex-sizing.reset-size10.size2{font-size:.2892960463em}.katex .fontsize-ensurer.reset-size10.size3,.katex .katex-sizing.reset-size10.size3{font-size:.337512054em}.katex .fontsize-ensurer.reset-size10.size4,.katex .katex-sizing.reset-size10.size4{font-size:.3857280617em}.katex .fontsize-ensurer.reset-size10.size5,.katex .katex-sizing.reset-size10.size5{font-size:.4339440694em}.katex .fontsize-ensurer.reset-size10.size6,.katex .katex-sizing.reset-size10.size6{font-size:.4821600771em}.katex .fontsize-ensurer.reset-size10.size7,.katex .katex-sizing.reset-size10.size7{font-size:.5785920926em}.katex .fontsize-ensurer.reset-size10.size8,.katex .katex-sizing.reset-size10.size8{font-size:.6943105111em}.katex .fontsize-ensurer.reset-size10.size9,.katex .katex-sizing.reset-size10.size9{font-size:.8331726133em}.katex .fontsize-ensurer.reset-size10.size10,.katex .katex-sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .katex-sizing.reset-size10.size11{font-size:1.1996142719em}.katex .fontsize-ensurer.reset-size11.size1,.katex .katex-sizing.reset-size11.size1{font-size:.2009646302em}.katex .fontsize-ensurer.reset-size11.size2,.katex .katex-sizing.reset-size11.size2{font-size:.2411575563em}.katex .fontsize-ensurer.reset-size11.size3,.katex .katex-sizing.reset-size11.size3{font-size:.2813504823em}.katex .fontsize-ensurer.reset-size11.size4,.katex .katex-sizing.reset-size11.size4{font-size:.3215434084em}.katex .fontsize-ensurer.reset-size11.size5,.katex .katex-sizing.reset-size11.size5{font-size:.3617363344em}.katex .fontsize-ensurer.reset-size11.size6,.katex .katex-sizing.reset-size11.size6{font-size:.4019292605em}.katex .fontsize-ensurer.reset-size11.size7,.katex .katex-sizing.reset-size11.size7{font-size:.4823151125em}.katex .fontsize-ensurer.reset-size11.size8,.katex .katex-sizing.reset-size11.size8{font-size:.578778135em}.katex .fontsize-ensurer.reset-size11.size9,.katex .katex-sizing.reset-size11.size9{font-size:.6945337621em}.katex .fontsize-ensurer.reset-size11.size10,.katex .katex-sizing.reset-size11.size10{font-size:.8336012862em}.katex .fontsize-ensurer.reset-size11.size11,.katex .katex-sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .katex-accent>.vlist-t,.katex .op-limits>.vlist-t{text-align:center}.katex .katex-accent .accent-body{position:relative}.katex .katex-accent .accent-body:not(.accent-full){width:0}.katex .katex-overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;min-width:1px}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}.katex .mtable .col-align-l>.vlist-t{text-align:left}.katex .mtable .col-align-r>.vlist-t{text-align:right}.katex .svg-align{text-align:left}.katex svg{fill:currentColor;stroke:currentColor;display:block;height:inherit;position:absolute;width:100%}.katex svg path{stroke:none}.katex svg{fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1}.katex img{border-style:none;max-height:none;max-width:none;min-height:0;min-width:0}.katex .katex-stretchy{display:block;overflow:hidden;position:relative;width:100%}.katex .katex-stretchy:after,.katex .katex-stretchy:before{content:""}.katex .hide-tail{overflow:hidden;position:relative;width:100%}.katex .halfarrow-left{left:0;overflow:hidden;position:absolute;width:50.2%}.katex .halfarrow-right{overflow:hidden;position:absolute;right:0;width:50.2%}.katex .brace-left{left:0;overflow:hidden;position:absolute;width:25.1%}.katex .brace-center{left:25%;overflow:hidden;position:absolute;width:50%}.katex .brace-right{overflow:hidden;position:absolute;right:0;width:25.1%}.katex .x-arrow-pad{padding:0 .5em}.katex .cd-arrow-pad{padding:0 .55556em 0 .27778em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox,.katex .fcolorbox{border:.04em solid;box-sizing:border-box}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap{margin-left:-.2em;margin-right:-.2em}.katex .katex-sout{border-bottom-style:solid;border-bottom-width:.08em}.katex .angl{border-right:.049em solid;border-top:.049em solid;box-sizing:border-box;margin-right:.03889em}.katex .anglpad{padding:0 .03889em}.katex .eqn-num:before{content:"(" counter(katexEqnNo) ")";counter-increment:katexEqnNo}.katex .mml-eqn-num:before{content:"(" counter(mmlEqnNo) ")";counter-increment:mmlEqnNo}.katex .mtr-glue{width:50%}.katex .cd-vert-arrow{display:inline-block;position:relative}.katex .cd-label-left{display:inline-block;position:absolute;right:calc(50% + .3em);text-align:left}.katex .cd-label-right{display:inline-block;left:calc(50% + .3em);position:absolute;text-align:right}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:block;text-align:center;white-space:nowrap}.katex-display>.katex>.katex-html{display:block;position:relative}.katex-display>.katex>.katex-html>.katex-tag{position:absolute;right:0}.katex-display.leqno>.katex>.katex-html>.katex-tag{left:0;right:auto}.katex-display.fleqn>.katex{padding-left:2em;text-align:left}body{counter-reset:katexEqnNo mmlEqnNo}