openclacky 1.1.1 → 1.1.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/README.md +28 -7
  4. data/lib/clacky/agent/llm_caller.rb +23 -1
  5. data/lib/clacky/agent/session_serializer.rb +6 -1
  6. data/lib/clacky/agent.rb +14 -5
  7. data/lib/clacky/anthropic_stream_aggregator.rb +135 -0
  8. data/lib/clacky/bedrock_stream_aggregator.rb +137 -0
  9. data/lib/clacky/cli.rb +9 -2
  10. data/lib/clacky/client.rb +146 -17
  11. data/lib/clacky/default_skills/onboard/SKILL.md +6 -2
  12. data/lib/clacky/default_skills/onboard/scripts/import_external_skills.rb +50 -6
  13. data/lib/clacky/openai_stream_aggregator.rb +130 -0
  14. data/lib/clacky/server/http_server.rb +2 -3
  15. data/lib/clacky/server/web_ui_controller.rb +8 -4
  16. data/lib/clacky/ui2/progress_handle.rb +77 -15
  17. data/lib/clacky/ui2/ui_controller.rb +4 -2
  18. data/lib/clacky/version.rb +1 -1
  19. data/lib/clacky/web/app.css +6 -4
  20. data/lib/clacky/web/i18n.js +6 -0
  21. data/lib/clacky/web/index.html +3 -1
  22. data/lib/clacky/web/sessions.js +152 -48
  23. data/lib/clacky/web/vendor/katex/auto-render.min.js +1 -0
  24. data/lib/clacky/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  25. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  26. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  27. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  28. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  29. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  30. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  31. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  32. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  33. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  34. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  35. data/lib/clacky/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  36. data/lib/clacky/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  37. data/lib/clacky/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  38. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  39. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  40. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  41. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  42. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  43. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  44. data/lib/clacky/web/vendor/katex/katex.min.css +1 -0
  45. data/lib/clacky/web/vendor/katex/katex.min.js +1 -0
  46. data/lib/clacky/web/ws-dispatcher.js +19 -4
  47. data/lib/clacky.rb +3 -0
  48. data/scripts/install.ps1 +14 -3
  49. metadata +28 -2
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "1.1.1"
4
+ VERSION = "1.1.2"
5
5
  end
@@ -943,9 +943,9 @@ body {
943
943
  align-self: center;
944
944
  }
945
945
  .session-item:hover .session-actions-btn { display: flex; }
946
- .session-actions-btn:hover {
947
- background: var(--color-border-primary);
948
- color: var(--color-text-primary);
946
+ .session-actions-btn:hover {
947
+ background: var(--color-border-primary);
948
+ color: var(--color-text-primary);
949
949
  }
950
950
 
951
951
  /* Pin icon in session name */
@@ -1808,6 +1808,8 @@ body {
1808
1808
  .msg-assistant em { font-style: italic; color: var(--color-text-secondary); }
1809
1809
  .msg-tool { background: var(--color-bg-primary); border: 1px solid var(--color-border-primary); font-family: monospace; font-size: 12px; color: var(--color-text-secondary); align-self: flex-start; }
1810
1810
  .msg-info { color: var(--color-text-secondary); font-size: 12px; align-self: center; font-style: italic; }
1811
+ .msg-info.msg-info-main { font-style: normal; }
1812
+ .msg-info-sub { color: var(--color-text-secondary); font-size: 11px; align-self: center; opacity: 0.7; margin-top: -27px; }
1811
1813
 
1812
1814
  /* ── Feedback request card ──────────────────────────────────────────────── */
1813
1815
  .feedback-card {
@@ -1926,7 +1928,7 @@ body {
1926
1928
  }
1927
1929
  .msg-success { color: var(--color-success); align-self: flex-start; font-size: 13px; }
1928
1930
  .tool-name { color: var(--color-warning); font-weight: 600; }
1929
- .progress-msg { color: var(--color-accent-primary); font-size: 12px; align-self: center; animation: pulse 1.2s infinite; }
1931
+ .progress-msg { color: var(--color-accent-primary); font-size: 12px; align-self: center; }
1930
1932
 
1931
1933
  /* ── Token usage line ────────────────────────────────────────────────────── */
1932
1934
  .token-usage-line {
@@ -37,6 +37,7 @@ const I18n = (() => {
37
37
  "chat.status.running": "running",
38
38
  "chat.status.error": "error",
39
39
  "chat.input.placeholder": "Message… (Enter to send, Shift+Enter for newline)",
40
+ "chat.input.placeholderRunning": "AI is working — you can still send extra info anytime...",
40
41
  "chat.btn.send": "Send",
41
42
  "chat.thinking": "Thinking…",
42
43
  "chat.retrying": "Retrying",
@@ -44,6 +45,8 @@ const I18n = (() => {
44
45
  "chat.history_start": "No more history",
45
46
  "chat.image_expired": "Expired",
46
47
  "chat.done": "Done — {{n}} iteration(s), {{cost}}",
48
+ "chat.done.duration": " · {{duration}}s",
49
+ "chat.done.cache": "Cache hit {{rate}}% ({{hits}}/{{total}}) · {{tokens}} tokens reused",
47
50
  "chat.interrupted": "Interrupted.",
48
51
  "chat.feedback_hint": "Or type your own answer below ↓",
49
52
  "chat.newMessageHint": "New messages ↓",
@@ -562,6 +565,7 @@ const I18n = (() => {
562
565
  "chat.status.running": "运行中",
563
566
  "chat.status.error": "出错",
564
567
  "chat.input.placeholder": "输入消息…(Enter 发送,Shift+Enter 换行)",
568
+ "chat.input.placeholderRunning": "AI 正在工作,你仍然可以随时补充新信息给它...",
565
569
  "chat.btn.send": "发送",
566
570
  "chat.thinking": "思考中…",
567
571
  "chat.retrying": "正在重试",
@@ -569,6 +573,8 @@ const I18n = (() => {
569
573
  "chat.history_start": "没有更多历史了",
570
574
  "chat.image_expired": "已过期",
571
575
  "chat.done": "完成 — {{n}} 步,{{cost}}",
576
+ "chat.done.duration": " · {{duration}}s",
577
+ "chat.done.cache": "缓存命中 {{rate}}% ({{hits}}/{{total}}) · 复用 {{tokens}} tokens",
572
578
  "chat.interrupted": "已中断。",
573
579
  "chat.feedback_hint": "或在下方输入框自由作答 ↓",
574
580
  "chat.newMessageHint": "有新消息 ↓",
@@ -5,6 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title id="page-title">{{BRAND_NAME}}</title>
7
7
  <link rel="icon" type="image/svg+xml" href="/icon.svg">
8
+ <link rel="stylesheet" href="/vendor/katex/katex.min.css">
8
9
  <link rel="stylesheet" href="/app.css">
9
10
  <script>
10
11
  // Inline theme init — must run before CSS renders to prevent flash of wrong theme.
@@ -905,7 +906,6 @@
905
906
  <div class="modal-box new-session-modal">
906
907
  <div class="modal-header">
907
908
  <h3 class="modal-title" data-i18n="sessions.modal.title">Create New Session</h3>
908
- <button id="new-session-modal-close" class="modal-close-btn" title="Close">×</button>
909
909
  </div>
910
910
  <div class="modal-body">
911
911
  <div class="modal-field">
@@ -997,6 +997,8 @@
997
997
 
998
998
 
999
999
  <script src="/marked.min.js"></script>
1000
+ <script src="/vendor/katex/katex.min.js"></script>
1001
+ <script src="/vendor/katex/auto-render.min.js"></script>
1000
1002
  <script src="/i18n.js"></script>
1001
1003
  <script src="/auth.js"></script>
1002
1004
  <script src="/theme.js"></script>
@@ -90,20 +90,67 @@ const Sessions = (() => {
90
90
  // if the marked library is unavailable.
91
91
  function _markedParse(text) {
92
92
  if (!text) return "";
93
+
94
+ // Extract math BEFORE marked so backslashes / underscores survive intact.
95
+ const math = [];
96
+ const PLACEHOLDER = (i) => `\u0000KTX${i}\u0000`;
97
+ let prepared = _extractMath(text, math, PLACEHOLDER);
98
+
99
+ let html;
93
100
  if (typeof marked !== "undefined") {
94
- // Custom renderer: open all links in a new tab
95
101
  const renderer = new marked.Renderer();
96
102
  renderer.link = function({ href, title, text }) {
97
103
  const titleAttr = title ? ` title="${title}"` : "";
98
104
  return `<a href="${href}"${titleAttr} target="_blank" rel="noopener noreferrer">${text}</a>`;
99
105
  };
100
- // Use marked with a few sensible defaults:
101
- // breaks: true — treat single newlines as <br> (matches chat UX expectations)
102
- // gfm: true — GitHub-flavoured markdown (tables, strikethrough, etc.)
103
- return marked.parse(text, { breaks: true, gfm: true, renderer });
106
+ html = marked.parse(prepared, { breaks: true, gfm: true, renderer });
107
+ } else {
108
+ html = escapeHtml(prepared).replace(/\n/g, "<br>");
109
+ }
110
+
111
+ if (math.length) {
112
+ html = html.replace(/\u0000KTX(\d+)\u0000/g, (_, i) => _renderMath(math[+i]));
113
+ }
114
+ return html;
115
+ }
116
+
117
+ // Pull $$...$$, \[...\], $...$, \(...\) out of `text` and replace each with a
118
+ // sentinel placeholder so marked won't mangle the LaTeX source. The matched
119
+ // segments are pushed (with display flag) onto `out` for later KaTeX rendering.
120
+ function _extractMath(text, out, placeholder) {
121
+ // Order matters: longest/most-specific delimiters first.
122
+ const patterns = [
123
+ { re: /\$\$([\s\S]+?)\$\$/g, display: true },
124
+ { re: /\\\[([\s\S]+?)\\\]/g, display: true },
125
+ { re: /\\\(([\s\S]+?)\\\)/g, display: false },
126
+ // Inline $...$: avoid $$, escaped \$, and prevent crossing newlines/blanks.
127
+ { re: /(^|[^\$])\$(?!\s)([^\$\n]+?)(?<!\s)\$(?!\d)/g, display: false, hasPrefix: true },
128
+ ];
129
+ let result = text;
130
+ for (const { re, display, hasPrefix } of patterns) {
131
+ result = result.replace(re, (m, a, b) => {
132
+ const body = hasPrefix ? b : a;
133
+ const idx = out.length;
134
+ out.push({ body, display });
135
+ return (hasPrefix ? a : "") + placeholder(idx);
136
+ });
137
+ }
138
+ return result;
139
+ }
140
+
141
+ function _renderMath({ body, display }) {
142
+ if (typeof katex === "undefined") {
143
+ return `<code>${escapeHtml((display ? "$$" : "$") + body + (display ? "$$" : "$"))}</code>`;
144
+ }
145
+ try {
146
+ return katex.renderToString(body, {
147
+ displayMode: display,
148
+ throwOnError: false,
149
+ output: "html",
150
+ });
151
+ } catch (e) {
152
+ return `<code class="katex-error">${escapeHtml(body)}</code>`;
104
153
  }
105
- // Fallback: plain escaped text with newlines preserved
106
- return escapeHtml(text).replace(/\n/g, "<br>");
107
154
  }
108
155
 
109
156
  // Build the collapsible thinking block HTML for a given rendered-HTML content string.
@@ -112,7 +159,7 @@ const Sessions = (() => {
112
159
  return `<details class="thinking-block">` +
113
160
  `<summary class="thinking-summary">` +
114
161
  `<span class="thinking-chevron">›</span>` +
115
- `<span class="thinking-label">Thought for a moment</span>` +
162
+ `<span class="thinking-label">Thoughts</span>` +
116
163
  `</summary>` +
117
164
  `<div class="thinking-body">${renderedHtml}</div>` +
118
165
  `</details>`;
@@ -349,9 +396,7 @@ const Sessions = (() => {
349
396
  document.getElementById("btn-welcome-new")
350
397
  .addEventListener("click", () => Sessions.create("general"));
351
398
 
352
- // Modal: close / cancel / create / overlay click
353
- document.getElementById("new-session-modal-close")
354
- .addEventListener("click", () => Sessions.closeNewSessionModal());
399
+ // Modal: cancel / create / overlay click
355
400
  document.getElementById("new-session-cancel")
356
401
  .addEventListener("click", () => Sessions.closeNewSessionModal());
357
402
  document.getElementById("new-session-create")
@@ -2051,6 +2096,15 @@ const Sessions = (() => {
2051
2096
  // Here we only update the interrupt button visibility.
2052
2097
  const interrupt = $("btn-interrupt");
2053
2098
  if (interrupt) interrupt.style.display = status === "running" ? "" : "none";
2099
+
2100
+ // Swap input placeholder so the user knows they can still send extra
2101
+ // info while the agent is working.
2102
+ const inp = $("user-input");
2103
+ if (inp) {
2104
+ const key = status === "running" ? "chat.input.placeholderRunning" : "chat.input.placeholder";
2105
+ inp.setAttribute("data-i18n-placeholder", key);
2106
+ inp.setAttribute("placeholder", I18n.t(key));
2107
+ }
2054
2108
  },
2055
2109
 
2056
2110
  /**
@@ -2410,13 +2464,19 @@ const Sessions = (() => {
2410
2464
  }
2411
2465
  },
2412
2466
 
2413
- appendInfo(text) {
2467
+ appendInfo(text, subline) {
2414
2468
  Sessions.collapseToolGroup();
2415
2469
  const messages = $("messages");
2416
2470
  const el = document.createElement("div");
2417
- el.className = "msg msg-info";
2471
+ el.className = subline ? "msg msg-info msg-info-main" : "msg msg-info";
2418
2472
  el.textContent = text;
2419
2473
  messages.appendChild(el);
2474
+ if (subline) {
2475
+ const sub = document.createElement("div");
2476
+ sub.className = "msg msg-info-sub";
2477
+ sub.textContent = subline;
2478
+ messages.appendChild(sub);
2479
+ }
2420
2480
  _scrollToBottomIfNeeded(messages);
2421
2481
  },
2422
2482
 
@@ -2490,11 +2550,58 @@ const Sessions = (() => {
2490
2550
  _getProgressState(id) {
2491
2551
  if (!id) return null;
2492
2552
  if (!Sessions._sessionProgress[id]) {
2493
- Sessions._sessionProgress[id] = { el: null, interval: null, startTime: null, type: null, displayText: null };
2553
+ Sessions._sessionProgress[id] = { el: null, interval: null, startTime: null, type: null, displayText: null, metadata: null, lastChunkAt: null };
2494
2554
  }
2495
2555
  return Sessions._sessionProgress[id];
2496
2556
  },
2497
2557
 
2558
+ // Compact a token count: 1234 → "1.2k", 12345 → "12k", 1234567 → "1.2M".
2559
+ _compactTokenCount(n) {
2560
+ if (n < 1000) return String(n);
2561
+ if (n < 1_000_000) {
2562
+ const k = n / 1000;
2563
+ return k >= 10 ? `${Math.floor(k)}k` : `${k.toFixed(1)}k`;
2564
+ }
2565
+ const m = n / 1_000_000;
2566
+ return m >= 10 ? `${Math.floor(m)}M` : `${m.toFixed(1)}M`;
2567
+ },
2568
+
2569
+ // Render LLM streaming output token count as "↓ 234 tokens".
2570
+ // Returns null when no positive output_tokens — matches CLI behaviour
2571
+ // (input is hidden mid-stream because most providers only ship
2572
+ // input_tokens with the final usage frame).
2573
+ _formatTokenSuffix(metadata) {
2574
+ if (!metadata) return null;
2575
+ const output = metadata.output_tokens;
2576
+ if (output == null || output <= 0) return null;
2577
+ return `↓ ${Sessions._compactTokenCount(output)} tokens`;
2578
+ },
2579
+
2580
+ // Compose the live progress line:
2581
+ // "<text>… (Ns · ↓N tokens · reasoning…)"
2582
+ // The "reasoning" tail surfaces inter-chunk silence so users see
2583
+ // the model is in extended thinking, not stuck. Threshold mirrors
2584
+ // ProgressHandle::IDLE_HINT_THRESHOLD_SECONDS. Animated dots avoid
2585
+ // duplicating the elapsed counter.
2586
+ _composeProgressLine(displayText, startTime, metadata, lastChunkAt) {
2587
+ const now = Date.now();
2588
+ const elapsed = startTime ? Math.floor((now - startTime) / 1000) : 0;
2589
+ const tokenStr = Sessions._formatTokenSuffix(metadata);
2590
+ const parts = [];
2591
+ if (elapsed > 0) parts.push(`${elapsed}s`);
2592
+ if (tokenStr) parts.push(tokenStr);
2593
+ if (tokenStr && lastChunkAt) {
2594
+ const idle = Math.floor((now - lastChunkAt) / 1000);
2595
+ if (idle >= 2) {
2596
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
2597
+ const frame = frames[Math.floor(now / 250) % frames.length];
2598
+ parts.push(`reasoning ${frame} `);
2599
+ }
2600
+ }
2601
+ if (parts.length === 0) return displayText;
2602
+ return `${displayText}… (${parts.join(" · ")})`;
2603
+ },
2604
+
2498
2605
  // Build the display label for a given progress type (pure — no side effects).
2499
2606
  _buildDisplayText(text, progress_type, metadata) {
2500
2607
  if (progress_type === "thinking") {
@@ -2530,23 +2637,19 @@ const Sessions = (() => {
2530
2637
 
2531
2638
  const el = document.createElement("div");
2532
2639
  el.className = "progress-msg";
2533
- const displayText = state.displayText;
2534
- // Show elapsed time immediately (not just after first setInterval tick)
2535
- const initialElapsed = Math.floor((Date.now() - state.startTime) / 1000);
2536
- el.textContent = initialElapsed > 0
2537
- ? `⟳ ${displayText}… (${initialElapsed}s)`
2538
- : `⟳ ${displayText}`;
2640
+ el.textContent = Sessions._composeProgressLine(state.displayText, state.startTime, state.metadata, state.lastChunkAt);
2539
2641
  messages.appendChild(el);
2540
2642
  state.el = el;
2541
2643
  _scrollToBottomIfNeeded(messages);
2542
2644
 
2543
- // Start elapsed time counter (update every second)
2645
+ // Tick at 250ms so streaming token counts feel live. The elapsed
2646
+ // counter only displays whole seconds, but token numbers update at
2647
+ // sub-second cadence on fast streams.
2544
2648
  state.interval = setInterval(() => {
2545
- const elapsed = Math.floor((Date.now() - state.startTime) / 1000);
2546
2649
  if (state.el) {
2547
- state.el.textContent = `⟳ ${displayText}… (${elapsed}s)`;
2650
+ state.el.textContent = Sessions._composeProgressLine(state.displayText, state.startTime, state.metadata, state.lastChunkAt);
2548
2651
  }
2549
- }, 1000);
2652
+ }, 250);
2550
2653
  },
2551
2654
 
2552
2655
  // Detach only the DOM element and timer for a session, preserving logical state
@@ -2568,54 +2671,53 @@ const Sessions = (() => {
2568
2671
  const sid = _activeId;
2569
2672
  if (!sid) return;
2570
2673
 
2571
- const newStartTime = startedAt || Date.now();
2572
- const newDisplayText = Sessions._buildDisplayText(text, progress_type, metadata);
2674
+ const newStartTime = startedAt || Date.now();
2573
2675
 
2574
- // If this session already has a visible progress indicator (DOM element
2575
- // attached), update it in-place instead of tear-down/rebuild. This avoids
2576
- // the jarring flicker when replay_live_state arrives shortly after the
2577
- // eager-attach on session switch.
2578
2676
  const existing = Sessions._sessionProgress[sid];
2579
2677
  if (existing && existing.el) {
2580
- // If the start time is the same (same progress phase, e.g. dedup replay),
2581
- // keep everything as-is not even the display text changes.
2678
+ // Same start time same progress phase. Most common case during LLM
2679
+ // streaming (token counts arriving every ~250ms with message: null).
2680
+ // Keep the existing displayText so the random "thinking" verb does
2681
+ // NOT churn on every chunk. Just refresh metadata; the interval tick
2682
+ // will repaint with fresh tokens.
2582
2683
  if (existing.startTime === newStartTime) {
2583
- existing.type = progress_type;
2684
+ existing.type = progress_type;
2685
+ existing.metadata = metadata || {};
2686
+ existing.lastChunkAt = Date.now();
2687
+ // Only adopt a new displayText if the server actually sent one.
2688
+ if (text) existing.displayText = Sessions._buildDisplayText(text, progress_type, metadata);
2584
2689
  return;
2585
2690
  }
2586
- // Different start time → new progress phase. Update state in-place and
2587
- // restart the interval, but reuse the existing DOM element so the user
2588
- // never sees the indicator disappear/reappear.
2691
+ // Different start time → new progress phase. Update state in-place
2692
+ // and reset the timer base, but reuse the existing DOM element so
2693
+ // the user never sees the indicator disappear/reappear.
2694
+ const newDisplayText = Sessions._buildDisplayText(text, progress_type, metadata);
2589
2695
  existing.type = progress_type;
2590
2696
  existing.startTime = newStartTime;
2591
2697
  existing.displayText = newDisplayText;
2592
- // Immediately refresh the text + elapsed counter
2593
- const elapsed = Math.floor((Date.now() - newStartTime) / 1000);
2594
- existing.el.textContent = elapsed > 0
2595
- ? `⟳ ${newDisplayText}… (${elapsed}s)`
2596
- : `⟳ ${newDisplayText}`;
2597
- // Restart interval with new startTime
2698
+ existing.metadata = metadata || {};
2699
+ existing.lastChunkAt = newStartTime;
2700
+ existing.el.textContent = Sessions._composeProgressLine(newDisplayText, newStartTime, metadata, existing.lastChunkAt);
2598
2701
  if (existing.interval) clearInterval(existing.interval);
2599
2702
  existing.interval = setInterval(() => {
2600
- const e = Math.floor((Date.now() - existing.startTime) / 1000);
2601
2703
  if (existing.el) {
2602
- existing.el.textContent = `⟳ ${existing.displayText}… (${e}s)`;
2704
+ existing.el.textContent = Sessions._composeProgressLine(existing.displayText, existing.startTime, existing.metadata, existing.lastChunkAt);
2603
2705
  }
2604
- }, 1000);
2706
+ }, 250);
2605
2707
  _scrollToBottomIfNeeded($("messages"));
2606
2708
  return;
2607
2709
  }
2608
2710
 
2609
2711
  // No existing visible progress — create from scratch.
2610
- // Clear any stale logical state first.
2611
2712
  Sessions.clearProgress(sid);
2612
2713
 
2613
2714
  const state = Sessions._getProgressState(sid);
2614
2715
  state.type = progress_type;
2615
2716
  state.startTime = newStartTime;
2616
- state.displayText = newDisplayText;
2717
+ state.displayText = Sessions._buildDisplayText(text, progress_type, metadata);
2718
+ state.metadata = metadata || {};
2719
+ state.lastChunkAt = newStartTime;
2617
2720
 
2618
- // Attach DOM + timer
2619
2721
  Sessions._attachProgressUI(sid);
2620
2722
  },
2621
2723
 
@@ -2655,6 +2757,8 @@ const Sessions = (() => {
2655
2757
  state.startTime = null;
2656
2758
  state.type = null;
2657
2759
  state.displayText = null;
2760
+ state.metadata = null;
2761
+ state.lastChunkAt = null;
2658
2762
  },
2659
2763
 
2660
2764
  // Delete all progress state for a session (used when session is removed).
@@ -0,0 +1 @@
1
+ !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("katex")):"function"==typeof define&&define.amd?define(["katex"],t):"object"==typeof exports?exports.renderMathInElement=t(require("katex")):e.renderMathInElement=t(e.katex)}("undefined"!=typeof self?self:this,(function(e){return function(){"use strict";var t={771:function(t){t.exports=e}},n={};function r(e){var o=n[e];if(void 0!==o)return o.exports;var i=n[e]={exports:{}};return t[e](i,i.exports,r),i.exports}r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,{a:t}),t},r.d=function(e,t){for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)};var o={};return function(){r.d(o,{default:function(){return d}});var e=r(771),t=r.n(e);const n=function(e,t,n){let r=n,o=0;const i=e.length;for(;r<t.length;){const n=t[r];if(o<=0&&t.slice(r,r+i)===e)return r;"\\"===n?r++:"{"===n?o++:"}"===n&&o--,r++}return-1},i=/^\\begin{/;var a=function(e,t){let r;const o=[],a=new RegExp("("+t.map((e=>e.left.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"))).join("|")+")");for(;r=e.search(a),-1!==r;){r>0&&(o.push({type:"text",data:e.slice(0,r)}),e=e.slice(r));const a=t.findIndex((t=>e.startsWith(t.left)));if(r=n(t[a].right,e,t[a].left.length),-1===r)break;const l=e.slice(0,r+t[a].right.length),s=i.test(l)?l:e.slice(t[a].left.length,r);o.push({type:"math",data:s,rawData:l,display:t[a].display}),e=e.slice(r+t[a].right.length)}return""!==e&&o.push({type:"text",data:e}),o};const l=function(e,n){const r=a(e,n.delimiters);if(1===r.length&&"text"===r[0].type)return null;const o=document.createDocumentFragment();for(let e=0;e<r.length;e++)if("text"===r[e].type)o.appendChild(document.createTextNode(r[e].data));else{const i=document.createElement("span");let a=r[e].data;n.displayMode=r[e].display;try{n.preProcess&&(a=n.preProcess(a)),t().render(a,i,n)}catch(i){if(!(i instanceof t().ParseError))throw i;n.errorCallback("KaTeX auto-render: Failed to parse `"+r[e].data+"` with ",i),o.appendChild(document.createTextNode(r[e].rawData));continue}o.appendChild(i)}return o},s=function(e,t){for(let n=0;n<e.childNodes.length;n++){const r=e.childNodes[n];if(3===r.nodeType){let o=r.textContent,i=r.nextSibling,a=0;for(;i&&i.nodeType===Node.TEXT_NODE;)o+=i.textContent,i=i.nextSibling,a++;const s=l(o,t);if(s){for(let e=0;e<a;e++)r.nextSibling.remove();n+=s.childNodes.length-1,e.replaceChild(s,r)}else n+=a}else if(1===r.nodeType){const e=" "+r.className+" ";-1===t.ignoredTags.indexOf(r.nodeName.toLowerCase())&&t.ignoredClasses.every((t=>-1===e.indexOf(" "+t+" ")))&&s(r,t)}}};var d=function(e,t){if(!e)throw new Error("No element provided to render");const n={};for(const e in t)t.hasOwnProperty(e)&&(n[e]=t[e]);n.delimiters=n.delimiters||[{left:"$$",right:"$$",display:!0},{left:"\\(",right:"\\)",display:!1},{left:"\\begin{equation}",right:"\\end{equation}",display:!0},{left:"\\begin{align}",right:"\\end{align}",display:!0},{left:"\\begin{alignat}",right:"\\end{alignat}",display:!0},{left:"\\begin{gather}",right:"\\end{gather}",display:!0},{left:"\\begin{CD}",right:"\\end{CD}",display:!0},{left:"\\[",right:"\\]",display:!0}],n.ignoredTags=n.ignoredTags||["script","noscript","style","textarea","pre","code","option"],n.ignoredClasses=n.ignoredClasses||[],n.errorCallback=n.errorCallback||console.error,n.macros=n.macros||{},s(e,n)}}(),o=o.default}()}));
@@ -0,0 +1 @@
1
+ @font-face{font-family:KaTeX_AMS;font-style:normal;font-weight:400;src:url(fonts/KaTeX_AMS-Regular.woff2) format("woff2")}@font-face{font-family:KaTeX_Caligraphic;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Caligraphic-Bold.woff2) format("woff2")}@font-face{font-family:KaTeX_Caligraphic;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Caligraphic-Regular.woff2) format("woff2")}@font-face{font-family:KaTeX_Fraktur;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Fraktur-Bold.woff2) format("woff2")}@font-face{font-family:KaTeX_Fraktur;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Fraktur-Regular.woff2) format("woff2")}@font-face{font-family:KaTeX_Main;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Main-Bold.woff2) format("woff2")}@font-face{font-family:KaTeX_Main;font-style:italic;font-weight:700;src:url(fonts/KaTeX_Main-BoldItalic.woff2) format("woff2")}@font-face{font-family:KaTeX_Main;font-style:italic;font-weight:400;src:url(fonts/KaTeX_Main-Italic.woff2) format("woff2")}@font-face{font-family:KaTeX_Main;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Main-Regular.woff2) format("woff2")}@font-face{font-family:KaTeX_Math;font-style:italic;font-weight:700;src:url(fonts/KaTeX_Math-BoldItalic.woff2) format("woff2")}@font-face{font-family:KaTeX_Math;font-style:italic;font-weight:400;src:url(fonts/KaTeX_Math-Italic.woff2) format("woff2")}@font-face{font-family:"KaTeX_SansSerif";font-style:normal;font-weight:700;src:url(fonts/KaTeX_SansSerif-Bold.woff2) format("woff2")}@font-face{font-family:"KaTeX_SansSerif";font-style:italic;font-weight:400;src:url(fonts/KaTeX_SansSerif-Italic.woff2) format("woff2")}@font-face{font-family:"KaTeX_SansSerif";font-style:normal;font-weight:400;src:url(fonts/KaTeX_SansSerif-Regular.woff2) format("woff2")}@font-face{font-family:KaTeX_Script;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Script-Regular.woff2) format("woff2")}@font-face{font-family:KaTeX_Size1;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size1-Regular.woff2) format("woff2")}@font-face{font-family:KaTeX_Size2;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size2-Regular.woff2) format("woff2")}@font-face{font-family:KaTeX_Size3;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size3-Regular.woff2) format("woff2")}@font-face{font-family:KaTeX_Size4;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size4-Regular.woff2) format("woff2")}@font-face{font-family:KaTeX_Typewriter;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Typewriter-Regular.woff2) format("woff2")}.katex{font:normal 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important;border-color:currentColor}.katex .katex-version:after{content:"0.16.11"}.katex .katex-mathml{clip:rect(1px,1px,1px,1px);border:0;height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.katex .katex-html>.newline{display:block}.katex .base{position:relative;white-space:nowrap;width:-webkit-min-content;width:-moz-min-content;width:min-content}.katex .base,.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 .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 .vbox{align-items:baseline;display:inline-flex;flex-direction:column}.katex .hbox{width:100%}.katex .hbox,.katex .thinbox{display:inline-flex;flex-direction:row}.katex .thinbox{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 .hdashline,.katex .hline,.katex .mfrac .frac-line,.katex .overline .overline-line,.katex .rule,.katex .underline .underline-line{min-height:1px}.katex .mspace{display:inline-block}.katex .clap,.katex .llap,.katex .rlap{position:relative;width:0}.katex .clap>.inner,.katex .llap>.inner,.katex .rlap>.inner{position:absolute}.katex .clap>.fix,.katex .llap>.fix,.katex .rlap>.fix{display:inline-block}.katex .llap>.inner{right:0}.katex .clap>.inner,.katex .rlap>.inner{left:0}.katex .clap>.inner>span{margin-left:-50%;margin-right:50%}.katex .rule{border:0 solid;display:inline-block;position:relative}.katex .hline,.katex .overline .overline-line,.katex .underline .underline-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .hdashline{border-bottom-style:dashed;display:inline-block;width:100%}.katex .sqrt>.root{margin-left:.2777777778em;margin-right:-.5555555556em}.katex .fontsize-ensurer.reset-size1.size1,.katex .sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .sizing.reset-size2.size1{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .sizing.reset-size2.size3{font-size:1.1666666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .sizing.reset-size2.size4{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .sizing.reset-size2.size6{font-size:1.6666666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .sizing.reset-size2.size10{font-size:3.4566666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .sizing.reset-size2.size11{font-size:4.1466666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .sizing.reset-size3.size1{font-size:.7142857143em}.katex .fontsize-ensurer.reset-size3.size2,.katex .sizing.reset-size3.size2{font-size:.8571428571em}.katex .fontsize-ensurer.reset-size3.size3,.katex .sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .sizing.reset-size3.size4{font-size:1.1428571429em}.katex .fontsize-ensurer.reset-size3.size5,.katex .sizing.reset-size3.size5{font-size:1.2857142857em}.katex .fontsize-ensurer.reset-size3.size6,.katex .sizing.reset-size3.size6{font-size:1.4285714286em}.katex .fontsize-ensurer.reset-size3.size7,.katex .sizing.reset-size3.size7{font-size:1.7142857143em}.katex .fontsize-ensurer.reset-size3.size8,.katex .sizing.reset-size3.size8{font-size:2.0571428571em}.katex .fontsize-ensurer.reset-size3.size9,.katex .sizing.reset-size3.size9{font-size:2.4685714286em}.katex .fontsize-ensurer.reset-size3.size10,.katex .sizing.reset-size3.size10{font-size:2.9628571429em}.katex .fontsize-ensurer.reset-size3.size11,.katex .sizing.reset-size3.size11{font-size:3.5542857143em}.katex .fontsize-ensurer.reset-size4.size1,.katex .sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .sizing.reset-size5.size1{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .sizing.reset-size5.size2{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .sizing.reset-size5.size3{font-size:.7777777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .sizing.reset-size5.size4{font-size:.8888888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .sizing.reset-size5.size6{font-size:1.1111111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .sizing.reset-size5.size7{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .sizing.reset-size5.size10{font-size:2.3044444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .sizing.reset-size5.size11{font-size:2.7644444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .sizing.reset-size7.size1{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .sizing.reset-size7.size3{font-size:.5833333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .sizing.reset-size7.size4{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .sizing.reset-size7.size6{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .sizing.reset-size7.size10{font-size:1.7283333333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .sizing.reset-size7.size11{font-size:2.0733333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .sizing.reset-size8.size1{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .sizing.reset-size8.size2{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .sizing.reset-size8.size3{font-size:.4861111111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .sizing.reset-size8.size4{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .sizing.reset-size8.size6{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .sizing.reset-size8.size7{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .sizing.reset-size8.size10{font-size:1.4402777778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .sizing.reset-size8.size11{font-size:1.7277777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .sizing.reset-size9.size1{font-size:.2893518519em}.katex .fontsize-ensurer.reset-size9.size2,.katex .sizing.reset-size9.size2{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .sizing.reset-size9.size3{font-size:.4050925926em}.katex .fontsize-ensurer.reset-size9.size4,.katex .sizing.reset-size9.size4{font-size:.462962963em}.katex .fontsize-ensurer.reset-size9.size5,.katex .sizing.reset-size9.size5{font-size:.5208333333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .sizing.reset-size9.size6{font-size:.5787037037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .sizing.reset-size9.size7{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .sizing.reset-size9.size8{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .sizing.reset-size9.size10{font-size:1.2002314815em}.katex .fontsize-ensurer.reset-size9.size11,.katex .sizing.reset-size9.size11{font-size:1.4398148148em}.katex .fontsize-ensurer.reset-size10.size1,.katex .sizing.reset-size10.size1{font-size:.2410800386em}.katex .fontsize-ensurer.reset-size10.size2,.katex .sizing.reset-size10.size2{font-size:.2892960463em}.katex .fontsize-ensurer.reset-size10.size3,.katex .sizing.reset-size10.size3{font-size:.337512054em}.katex .fontsize-ensurer.reset-size10.size4,.katex .sizing.reset-size10.size4{font-size:.3857280617em}.katex .fontsize-ensurer.reset-size10.size5,.katex .sizing.reset-size10.size5{font-size:.4339440694em}.katex .fontsize-ensurer.reset-size10.size6,.katex .sizing.reset-size10.size6{font-size:.4821600771em}.katex .fontsize-ensurer.reset-size10.size7,.katex .sizing.reset-size10.size7{font-size:.5785920926em}.katex .fontsize-ensurer.reset-size10.size8,.katex .sizing.reset-size10.size8{font-size:.6943105111em}.katex .fontsize-ensurer.reset-size10.size9,.katex .sizing.reset-size10.size9{font-size:.8331726133em}.katex .fontsize-ensurer.reset-size10.size10,.katex .sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .sizing.reset-size10.size11{font-size:1.1996142719em}.katex .fontsize-ensurer.reset-size11.size1,.katex .sizing.reset-size11.size1{font-size:.2009646302em}.katex .fontsize-ensurer.reset-size11.size2,.katex .sizing.reset-size11.size2{font-size:.2411575563em}.katex .fontsize-ensurer.reset-size11.size3,.katex .sizing.reset-size11.size3{font-size:.2813504823em}.katex .fontsize-ensurer.reset-size11.size4,.katex .sizing.reset-size11.size4{font-size:.3215434084em}.katex .fontsize-ensurer.reset-size11.size5,.katex .sizing.reset-size11.size5{font-size:.3617363344em}.katex .fontsize-ensurer.reset-size11.size6,.katex .sizing.reset-size11.size6{font-size:.4019292605em}.katex .fontsize-ensurer.reset-size11.size7,.katex .sizing.reset-size11.size7{font-size:.4823151125em}.katex .fontsize-ensurer.reset-size11.size8,.katex .sizing.reset-size11.size8{font-size:.578778135em}.katex .fontsize-ensurer.reset-size11.size9,.katex .sizing.reset-size11.size9{font-size:.6945337621em}.katex .fontsize-ensurer.reset-size11.size10,.katex .sizing.reset-size11.size10{font-size:.8336012862em}.katex .fontsize-ensurer.reset-size11.size11,.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 .accent>.vlist-t,.katex .op-limits>.vlist-t{text-align:center}.katex .accent .accent-body{position:relative}.katex .accent .accent-body:not(.accent-full){width:0}.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;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;display:block;height:inherit;position:absolute;width:100%}.katex svg path{stroke:none}.katex img{border-style:none;max-height:none;max-width:none;min-height:0;min-width:0}.katex .stretchy{display:block;overflow:hidden;position:relative;width:100%}.katex .stretchy:after,.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 .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>.tag{position:absolute;right:0}.katex-display.leqno>.katex>.katex-html>.tag{left:0;right:auto}.katex-display.fleqn>.katex{padding-left:2em;text-align:left}body{counter-reset:katexEqnNo mmlEqnNo}