openclacky 1.1.0 → 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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -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/skill_manager.rb +18 -5
  7. data/lib/clacky/agent.rb +14 -5
  8. data/lib/clacky/anthropic_stream_aggregator.rb +135 -0
  9. data/lib/clacky/bedrock_stream_aggregator.rb +137 -0
  10. data/lib/clacky/brand_config.rb +68 -15
  11. data/lib/clacky/cli.rb +18 -19
  12. data/lib/clacky/client.rb +146 -17
  13. data/lib/clacky/default_skills/onboard/SKILL.md +6 -2
  14. data/lib/clacky/default_skills/onboard/scripts/import_external_skills.rb +50 -6
  15. data/lib/clacky/openai_stream_aggregator.rb +130 -0
  16. data/lib/clacky/server/channel/adapters/weixin/adapter.rb +169 -6
  17. data/lib/clacky/server/channel/channel_ui_controller.rb +6 -0
  18. data/lib/clacky/server/http_server.rb +9 -3
  19. data/lib/clacky/server/web_ui_controller.rb +8 -4
  20. data/lib/clacky/tools/terminal.rb +11 -0
  21. data/lib/clacky/ui2/components/input_area.rb +10 -1
  22. data/lib/clacky/ui2/components/todo_area.rb +22 -2
  23. data/lib/clacky/ui2/layout_manager.rb +70 -14
  24. data/lib/clacky/ui2/progress_handle.rb +86 -15
  25. data/lib/clacky/ui2/ui_controller.rb +47 -7
  26. data/lib/clacky/utils/logger.rb +7 -0
  27. data/lib/clacky/version.rb +1 -1
  28. data/lib/clacky/web/app.css +6 -4
  29. data/lib/clacky/web/i18n.js +21 -6
  30. data/lib/clacky/web/index.html +8 -6
  31. data/lib/clacky/web/sessions.js +171 -58
  32. data/lib/clacky/web/vendor/katex/auto-render.min.js +1 -0
  33. data/lib/clacky/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  34. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  35. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  36. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  37. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  38. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  39. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  40. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  41. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  42. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  43. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  44. data/lib/clacky/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  45. data/lib/clacky/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  46. data/lib/clacky/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  47. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  48. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  49. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  50. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  51. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  52. data/lib/clacky/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  53. data/lib/clacky/web/vendor/katex/katex.min.css +1 -0
  54. data/lib/clacky/web/vendor/katex/katex.min.js +1 -0
  55. data/lib/clacky/web/ws-dispatcher.js +19 -4
  56. data/lib/clacky.rb +3 -0
  57. data/scripts/build/src/install.sh.cc +15 -5
  58. data/scripts/install.ps1 +14 -3
  59. data/scripts/install.sh +15 -5
  60. metadata +28 -2
@@ -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
  /**
@@ -2065,6 +2119,7 @@ const Sessions = (() => {
2065
2119
 
2066
2120
  /** Update the session info bar below the chat header with current session metadata. */
2067
2121
  updateInfoBar(s) {
2122
+ this._lastSession = s;
2068
2123
  if (!s) {
2069
2124
  // Hide all spans when no session
2070
2125
  ["sib-id", "sib-status", "sib-dir", "sib-mode", "sib-model", "sib-tasks", "sib-cost"].forEach(id => {
@@ -2103,8 +2158,8 @@ const Sessions = (() => {
2103
2158
  const sibDir = $("sib-dir");
2104
2159
  if (sibDir && s.working_dir) {
2105
2160
  sibDir.textContent = s.working_dir;
2106
- sibDir.title = s.working_dir + " (click to change)";
2107
- // Store session ID for later use
2161
+ sibDir.title = `${s.working_dir} (${I18n.t("sib.dir.tooltip")})`;
2162
+ sibDir.dataset.workingDir = s.working_dir;
2108
2163
  sibDir.dataset.sessionId = s.id;
2109
2164
  }
2110
2165
 
@@ -2138,7 +2193,7 @@ const Sessions = (() => {
2138
2193
 
2139
2194
  // Tasks
2140
2195
  const sibTasks = $("sib-tasks");
2141
- if (sibTasks) sibTasks.textContent = `${s.total_tasks || 0} tasks`;
2196
+ if (sibTasks) sibTasks.textContent = I18n.t("sessions.metaTasks", { n: s.total_tasks || 0 });
2142
2197
 
2143
2198
  // Cost — show N/A when pricing is unknown (estimated)
2144
2199
  const sibCost = $("sib-cost");
@@ -2409,13 +2464,19 @@ const Sessions = (() => {
2409
2464
  }
2410
2465
  },
2411
2466
 
2412
- appendInfo(text) {
2467
+ appendInfo(text, subline) {
2413
2468
  Sessions.collapseToolGroup();
2414
2469
  const messages = $("messages");
2415
2470
  const el = document.createElement("div");
2416
- el.className = "msg msg-info";
2471
+ el.className = subline ? "msg msg-info msg-info-main" : "msg msg-info";
2417
2472
  el.textContent = text;
2418
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
+ }
2419
2480
  _scrollToBottomIfNeeded(messages);
2420
2481
  },
2421
2482
 
@@ -2489,11 +2550,58 @@ const Sessions = (() => {
2489
2550
  _getProgressState(id) {
2490
2551
  if (!id) return null;
2491
2552
  if (!Sessions._sessionProgress[id]) {
2492
- 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 };
2493
2554
  }
2494
2555
  return Sessions._sessionProgress[id];
2495
2556
  },
2496
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
+
2497
2605
  // Build the display label for a given progress type (pure — no side effects).
2498
2606
  _buildDisplayText(text, progress_type, metadata) {
2499
2607
  if (progress_type === "thinking") {
@@ -2529,23 +2637,19 @@ const Sessions = (() => {
2529
2637
 
2530
2638
  const el = document.createElement("div");
2531
2639
  el.className = "progress-msg";
2532
- const displayText = state.displayText;
2533
- // Show elapsed time immediately (not just after first setInterval tick)
2534
- const initialElapsed = Math.floor((Date.now() - state.startTime) / 1000);
2535
- el.textContent = initialElapsed > 0
2536
- ? `⟳ ${displayText}… (${initialElapsed}s)`
2537
- : `⟳ ${displayText}`;
2640
+ el.textContent = Sessions._composeProgressLine(state.displayText, state.startTime, state.metadata, state.lastChunkAt);
2538
2641
  messages.appendChild(el);
2539
2642
  state.el = el;
2540
2643
  _scrollToBottomIfNeeded(messages);
2541
2644
 
2542
- // 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.
2543
2648
  state.interval = setInterval(() => {
2544
- const elapsed = Math.floor((Date.now() - state.startTime) / 1000);
2545
2649
  if (state.el) {
2546
- state.el.textContent = `⟳ ${displayText}… (${elapsed}s)`;
2650
+ state.el.textContent = Sessions._composeProgressLine(state.displayText, state.startTime, state.metadata, state.lastChunkAt);
2547
2651
  }
2548
- }, 1000);
2652
+ }, 250);
2549
2653
  },
2550
2654
 
2551
2655
  // Detach only the DOM element and timer for a session, preserving logical state
@@ -2567,54 +2671,53 @@ const Sessions = (() => {
2567
2671
  const sid = _activeId;
2568
2672
  if (!sid) return;
2569
2673
 
2570
- const newStartTime = startedAt || Date.now();
2571
- const newDisplayText = Sessions._buildDisplayText(text, progress_type, metadata);
2674
+ const newStartTime = startedAt || Date.now();
2572
2675
 
2573
- // If this session already has a visible progress indicator (DOM element
2574
- // attached), update it in-place instead of tear-down/rebuild. This avoids
2575
- // the jarring flicker when replay_live_state arrives shortly after the
2576
- // eager-attach on session switch.
2577
2676
  const existing = Sessions._sessionProgress[sid];
2578
2677
  if (existing && existing.el) {
2579
- // If the start time is the same (same progress phase, e.g. dedup replay),
2580
- // 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.
2581
2683
  if (existing.startTime === newStartTime) {
2582
- 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);
2583
2689
  return;
2584
2690
  }
2585
- // Different start time → new progress phase. Update state in-place and
2586
- // restart the interval, but reuse the existing DOM element so the user
2587
- // 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);
2588
2695
  existing.type = progress_type;
2589
2696
  existing.startTime = newStartTime;
2590
2697
  existing.displayText = newDisplayText;
2591
- // Immediately refresh the text + elapsed counter
2592
- const elapsed = Math.floor((Date.now() - newStartTime) / 1000);
2593
- existing.el.textContent = elapsed > 0
2594
- ? `⟳ ${newDisplayText}… (${elapsed}s)`
2595
- : `⟳ ${newDisplayText}`;
2596
- // Restart interval with new startTime
2698
+ existing.metadata = metadata || {};
2699
+ existing.lastChunkAt = newStartTime;
2700
+ existing.el.textContent = Sessions._composeProgressLine(newDisplayText, newStartTime, metadata, existing.lastChunkAt);
2597
2701
  if (existing.interval) clearInterval(existing.interval);
2598
2702
  existing.interval = setInterval(() => {
2599
- const e = Math.floor((Date.now() - existing.startTime) / 1000);
2600
2703
  if (existing.el) {
2601
- existing.el.textContent = `⟳ ${existing.displayText}… (${e}s)`;
2704
+ existing.el.textContent = Sessions._composeProgressLine(existing.displayText, existing.startTime, existing.metadata, existing.lastChunkAt);
2602
2705
  }
2603
- }, 1000);
2706
+ }, 250);
2604
2707
  _scrollToBottomIfNeeded($("messages"));
2605
2708
  return;
2606
2709
  }
2607
2710
 
2608
2711
  // No existing visible progress — create from scratch.
2609
- // Clear any stale logical state first.
2610
2712
  Sessions.clearProgress(sid);
2611
2713
 
2612
2714
  const state = Sessions._getProgressState(sid);
2613
2715
  state.type = progress_type;
2614
2716
  state.startTime = newStartTime;
2615
- state.displayText = newDisplayText;
2717
+ state.displayText = Sessions._buildDisplayText(text, progress_type, metadata);
2718
+ state.metadata = metadata || {};
2719
+ state.lastChunkAt = newStartTime;
2616
2720
 
2617
- // Attach DOM + timer
2618
2721
  Sessions._attachProgressUI(sid);
2619
2722
  },
2620
2723
 
@@ -2654,6 +2757,8 @@ const Sessions = (() => {
2654
2757
  state.startTime = null;
2655
2758
  state.type = null;
2656
2759
  state.displayText = null;
2760
+ state.metadata = null;
2761
+ state.lastChunkAt = null;
2657
2762
  },
2658
2763
 
2659
2764
  // Delete all progress state for a session (used when session is removed).
@@ -3208,9 +3313,9 @@ const Sessions = (() => {
3208
3313
  if (dirEl) {
3209
3314
  e.stopPropagation();
3210
3315
  const sessionId = dirEl.dataset.sessionId;
3211
- const currentDir = dirEl.title.replace(" (click to change)", "");
3212
-
3213
- const newDir = await Modal.prompt("Change working directory:", currentDir);
3316
+ const currentDir = dirEl.dataset.workingDir || dirEl.textContent;
3317
+
3318
+ const newDir = await Modal.prompt(I18n.t("sib.dir.changePrompt"), currentDir);
3214
3319
  if (newDir && newDir !== currentDir) {
3215
3320
  _changeWorkingDirectory(sessionId, newDir);
3216
3321
  }
@@ -3280,7 +3385,10 @@ const Sessions = (() => {
3280
3385
  }
3281
3386
 
3282
3387
  function _populateSessionActionsDropdown(dd, sessionId) {
3283
- const t = (key, fallback) => (window.I18n && I18n.t(key)) || fallback;
3388
+ const t = (key, fallback) => {
3389
+ const s = I18n.t(key);
3390
+ return (s && s !== key) ? s : fallback;
3391
+ };
3284
3392
  dd.innerHTML = "";
3285
3393
 
3286
3394
  // Download item
@@ -3320,7 +3428,7 @@ const Sessions = (() => {
3320
3428
  if (!res.ok) {
3321
3429
  let msg = `HTTP ${res.status}`;
3322
3430
  try { const data = await res.json(); if (data.error) msg = data.error; } catch (_) {}
3323
- alert((window.I18n && I18n.t("sessions.export.failed")) || "Failed to download session: " + msg);
3431
+ alert(I18n.t("sessions.export.failed") + ": " + msg);
3324
3432
  return;
3325
3433
  }
3326
3434
  const blob = await res.blob();
@@ -3342,7 +3450,7 @@ const Sessions = (() => {
3342
3450
  setTimeout(() => URL.revokeObjectURL(url), 1000);
3343
3451
  } catch (err) {
3344
3452
  console.error("Session export failed:", err);
3345
- alert(((window.I18n && I18n.t("sessions.export.failed")) || "Failed to download session") + ": " + err.message);
3453
+ alert(I18n.t("sessions.export.failed") + ": " + err.message);
3346
3454
  } finally {
3347
3455
  if (btnEl) {
3348
3456
  try { btnEl.disabled = wasDisabled; } catch (_) {}
@@ -3370,7 +3478,8 @@ const Sessions = (() => {
3370
3478
  const sibDir = $("sib-dir");
3371
3479
  if (sibDir) {
3372
3480
  sibDir.textContent = newDir;
3373
- sibDir.title = newDir + " (click to change)";
3481
+ sibDir.title = `${newDir} (${I18n.t("sib.dir.tooltip")})`;
3482
+ sibDir.dataset.workingDir = newDir;
3374
3483
  }
3375
3484
 
3376
3485
  console.log(`Changed session ${sessionId} directory to ${newDir}`);
@@ -3381,3 +3490,7 @@ const Sessions = (() => {
3381
3490
  }
3382
3491
 
3383
3492
  })();
3493
+
3494
+ document.addEventListener("langchange", () => {
3495
+ if (Sessions._lastSession) Sessions.updateInfoBar(Sessions._lastSession);
3496
+ });
@@ -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}