pygpt-net 2.6.38__py3-none-any.whl → 2.6.39__py3-none-any.whl

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.
@@ -46,30 +46,29 @@ class Body:
46
46
  <script type="text/javascript" src="qrc:///js/highlight.min.js"></script>
47
47
  <script type="text/javascript" src="qrc:///js/katex.min.js"></script>
48
48
  <script>
49
- if (hljs) {
49
+ if (typeof hljs !== 'undefined') {
50
50
  hljs.configure({
51
51
  ignoreUnescapedHTML: true,
52
52
  });
53
- }
54
- let DEBUG_MODE = false; // allow dynamic enabling via debug console
53
+ }
54
+ const streamQ = [];
55
+ let DEBUG_MODE = false;
55
56
  let bridgeConnected = false;
56
57
  let streamHandler;
57
58
  let nodeHandler;
58
59
  let nodeReplaceHandler;
59
- let scrollTimeout = null;
60
60
  let prevScroll = 0;
61
61
  let bridge;
62
- let streamQ = [];
63
62
  let streamRAF = 0;
63
+ let batching = false;
64
+ let needScroll = false;
64
65
  let pid = """
65
66
  _HTML_P2 = """
66
67
  let collapsed_idx = [];
67
- let domOutputStream = document.getElementById('_append_output_');
68
- let domOutput = document.getElementById('_output_');
69
- let domInput = document.getElementById('_input_');
68
+ let domOutputStream = null;
70
69
  let domLastCodeBlock = null;
71
70
  let domLastParagraphBlock = null;
72
- let htmlBuffer = "";
71
+ let domStreamMsg = null;
73
72
  let tips = """
74
73
  _HTML_P3 = """;
75
74
  let tips_hidden = false;
@@ -78,70 +77,77 @@ class Body:
78
77
  let highlightScheduled = false;
79
78
  let pendingHighlightRoot = null;
80
79
  let pendingHighlightMath = false;
81
- let highlightRAF = 0; // RAF id for highlight batcher
80
+ let highlightRAF = 0;
82
81
  let scrollScheduled = false;
83
82
 
84
- // Auto-follow state: when false, live stream auto-scroll is suppressed
85
83
  let autoFollow = true;
86
84
  let lastScrollTop = 0;
87
- // Tracks whether user has performed any scroll-related interaction
88
85
  let userInteracted = false;
89
- const AUTO_FOLLOW_REENABLE_PX = 8; // px from bottom to re-enable auto-follow
86
+ const AUTO_FOLLOW_REENABLE_PX = 8;
90
87
 
91
- // FAB thresholds
92
- const SHOW_DOWN_THRESHOLD_PX = 0; // show "down" only when farther than this from bottom
93
- let currentFabAction = 'none'; // tracks current FAB state to avoid redundant work
88
+ const SHOW_DOWN_THRESHOLD_PX = 0;
89
+ let currentFabAction = 'none';
94
90
 
95
- // timers
96
91
  let tipsTimers = [];
97
92
 
98
- // observers
99
- let roDoc = null;
100
- let roContainer = null;
101
-
102
- // FAB (scroll-to-top/bottom) scheduling
103
93
  let scrollFabUpdateScheduled = false;
104
94
 
105
- // Streaming micro-batching config
106
- const STREAM_MAX_PER_FRAME = 64; // defensive upper bound of operations per frame
107
- const STREAM_EMERGENCY_COALESCE_LEN = 1500; // when queue length is high, coalesce aggressively
95
+ const STREAM_MAX_PER_FRAME = 8;
96
+ const STREAM_EMERGENCY_COALESCE_LEN = 1500;
97
+
98
+ Object.defineProperty(window,'SE',{get(){return document.scrollingElement || document.documentElement;}});
99
+
100
+ let wheelHandler = null;
101
+ let scrollHandler = null;
102
+ let resizeHandler = null;
103
+ let fabClickHandler = null;
104
+ let containerMouseOverHandler = null;
105
+ let containerMouseOutHandler = null;
106
+ let containerClickHandler = null;
107
+ let keydownHandler = null;
108
+ let docClickFocusHandler = null;
109
+
110
+ let fabFreezeUntil = 0;
111
+ const FAB_TOGGLE_DEBOUNCE_MS = 100;
108
112
 
109
- // clear previous references
110
113
  function resetEphemeralDomRefs() {
111
114
  domLastCodeBlock = null;
112
115
  domLastParagraphBlock = null;
116
+ domStreamMsg = null;
113
117
  }
114
118
  function dropIfDetached() {
115
119
  if (domLastCodeBlock && !domLastCodeBlock.isConnected) domLastCodeBlock = null;
116
120
  if (domLastParagraphBlock && !domLastParagraphBlock.isConnected) domLastParagraphBlock = null;
121
+ if (domStreamMsg && !domStreamMsg.isConnected) domStreamMsg = null;
117
122
  }
118
123
  function stopTipsTimers() {
119
124
  tipsTimers.forEach(clearTimeout);
120
125
  tipsTimers = [];
121
126
  }
122
127
 
123
- function teardown() {
124
- // Cancel timers/RAF/observers to prevent background CPU usage
128
+ function cleanup() {
129
+ try { if (highlightRAF) { cancelAnimationFrame(highlightRAF); highlightRAF = 0; } } catch (e) {}
130
+ try { if (streamRAF) { cancelAnimationFrame(streamRAF); streamRAF = 0; } } catch (e) {}
125
131
  stopTipsTimers();
126
- try {
127
- if (streamRAF) {
128
- cancelAnimationFrame(streamRAF);
129
- streamRAF = 0;
130
- }
131
- if (highlightRAF) {
132
- cancelAnimationFrame(highlightRAF);
133
- highlightRAF = 0;
134
- }
135
- } catch (e) { /* ignore */ }
136
- // Clear streaming queue to release memory immediately
132
+ try { bridgeDisconnect(); } catch (e) {}
133
+ if (wheelHandler) document.removeEventListener('wheel', wheelHandler, { passive: true });
134
+ if (scrollHandler) window.removeEventListener('scroll', scrollHandler, { passive: true });
135
+ if (resizeHandler) window.removeEventListener('resize', resizeHandler, { passive: true });
136
+ if (fabClickHandler && els.scrollFab) els.scrollFab.removeEventListener('click', fabClickHandler, { passive: false });
137
+ if (containerMouseOverHandler && els.container) els.container.removeEventListener('mouseover', containerMouseOverHandler, { passive: true });
138
+ if (containerMouseOutHandler && els.container) els.container.removeEventListener('mouseout', containerMouseOutHandler, { passive: true });
139
+ if (containerClickHandler && els.container) els.container.removeEventListener('click', containerClickHandler, { passive: false });
140
+ if (keydownHandler) document.removeEventListener('keydown', keydownHandler, { passive: false });
141
+ if (docClickFocusHandler) document.removeEventListener('click', docClickFocusHandler, { passive: true });
137
142
  streamQ.length = 0;
138
- scrollFabUpdateScheduled = false;
139
- scrollScheduled = false;
140
- highlightScheduled = false;
143
+ collapsed_idx.length = 0;
144
+ resetEphemeralDomRefs();
145
+ els = {};
146
+ try { history.scrollRestoration = "auto"; } catch (e) {}
141
147
  }
142
148
 
143
149
  history.scrollRestoration = "manual";
144
- document.addEventListener('keydown', function(event) {
150
+ keydownHandler = function(event) {
145
151
  if (event.ctrlKey && event.key === 'f') {
146
152
  window.location.href = 'bridge://open_find:' + pid;
147
153
  event.preventDefault();
@@ -150,12 +156,17 @@ class Body:
150
156
  window.location.href = 'bridge://escape';
151
157
  event.preventDefault();
152
158
  }
153
- });
154
- document.addEventListener('click', function(event) {
159
+ };
160
+ document.addEventListener('keydown', keydownHandler, { passive: false });
161
+
162
+ docClickFocusHandler = function(event) {
163
+ if (event.target.closest('#scrollFab')) return;
155
164
  if (event.target.tagName !== 'A' && !event.target.closest('a')) {
156
165
  window.location.href = 'bridge://focus';
157
166
  }
158
- });
167
+ };
168
+ document.addEventListener('click', docClickFocusHandler, { passive: true });
169
+
159
170
  function log(text) {
160
171
  if (bridge) {
161
172
  bridge.log(text);
@@ -171,16 +182,13 @@ class Body:
171
182
  els.footer = document.getElementById('_footer_');
172
183
  els.loader = document.getElementById('_loader_');
173
184
  els.tips = document.getElementById('tips');
174
- // FAB refs
175
185
  els.scrollFab = document.getElementById('scrollFab');
176
186
  els.scrollFabIcon = document.getElementById('scrollFabIcon');
177
187
  }
188
+
178
189
  function bridgeConnect() {
179
- // Idempotent connect
180
- if (!bridge || !bridge.chunk || typeof bridge.chunk.connect !== 'function') return false;
190
+ if (!bridge) return false;
181
191
  if (bridgeConnected) return true;
182
-
183
- // Ensure handler exists and is stable (same identity for disconnect/connect)
184
192
  if (!streamHandler) {
185
193
  streamHandler = (name, html, chunk, replace, isCode) => {
186
194
  appendStream(name, html, chunk, replace, isCode);
@@ -193,9 +201,9 @@ class Body:
193
201
  };
194
202
  }
195
203
  try {
196
- bridge.chunk.connect(streamHandler);
197
- bridge.node.connect(nodeHandler);
198
- bridge.nodeReplace.connect(nodeReplaceHandler);
204
+ if (bridge.chunk && typeof bridge.chunk.connect === 'function') bridge.chunk.connect(streamHandler);
205
+ if (bridge.node && typeof bridge.node.connect === 'function') bridge.node.connect(nodeHandler);
206
+ if (bridge.nodeReplace && typeof bridge.nodeReplace.connect === 'function') bridge.nodeReplace.connect(nodeReplaceHandler);
199
207
  bridgeConnected = true;
200
208
  return true;
201
209
  } catch (e) {
@@ -203,29 +211,24 @@ class Body:
203
211
  return false;
204
212
  }
205
213
  }
206
-
207
214
  function bridgeDisconnect() {
208
- // Idempotent disconnect
209
- if (!bridge || !bridge.chunk || typeof bridge.chunk.disconnect !== 'function') return false;
215
+ if (!bridge) return false;
210
216
  if (!bridgeConnected) return true;
211
-
212
217
  try {
213
- bridge.chunk.disconnect(streamHandler);
214
- bridge.node.disconnect(nodeHandler);
215
- bridge.nodeReplace.disconnect(nodeReplaceHandler);
216
- } catch (e) { /* ignore */ }
218
+ if (bridge.chunk && typeof bridge.chunk.disconnect === 'function') bridge.chunk.disconnect(streamHandler);
219
+ if (bridge.node && typeof bridge.node.disconnect === 'function') bridge.node.disconnect(nodeHandler);
220
+ if (bridge.nodeReplace && typeof bridge.nodeReplace.disconnect === 'function') bridge.nodeReplace.disconnect(nodeReplaceHandler);
221
+ } catch (e) { }
217
222
  bridgeConnected = false;
218
-
219
- // Stop scheduled work and release pending chunks immediately
220
- try { if (streamRAF) { cancelAnimationFrame(streamRAF); streamRAF = 0; } } catch (e) { /* ignore */ }
223
+ try { if (streamRAF) { cancelAnimationFrame(streamRAF); streamRAF = 0; } } catch (e) { }
221
224
  streamQ.length = 0;
222
225
  return true;
223
226
  }
224
-
225
227
  function bridgeReconnect() {
226
228
  bridgeDisconnect();
227
229
  return bridgeConnect();
228
230
  }
231
+
229
232
  function scheduleHighlight(root, withMath = true) {
230
233
  const scope = root && root.nodeType === 1 ? root : document;
231
234
  if (!pendingHighlightRoot || pendingHighlightRoot === document) {
@@ -237,7 +240,6 @@ class Body:
237
240
  if (highlightScheduled) return;
238
241
  highlightScheduled = true;
239
242
  if (highlightRAF) {
240
- // Ensure we do not queue multiple highlight frames
241
243
  cancelAnimationFrame(highlightRAF);
242
244
  highlightRAF = 0;
243
245
  }
@@ -258,14 +260,10 @@ class Body:
258
260
  });
259
261
  if (withMath) {
260
262
  renderMath(root);
261
- if (DEBUG_MODE) log("math");
262
263
  }
263
- if (DEBUG_MODE) log("execute highlight");
264
264
  }
265
265
  function highlightCode(withMath = true, root = null) {
266
- if (DEBUG_MODE) log("queue highlight, withMath: " + withMath);
267
- highlightCodeInternal(root || document, withMath); // prevent blink on fast updates
268
- // scheduleHighlight(root || document, withMath); // disabled
266
+ highlightCodeInternal(root || document, withMath);
269
267
  }
270
268
  function hideTips() {
271
269
  if (tips_hidden) return;
@@ -304,7 +302,6 @@ class Body:
304
302
  showNextTip();
305
303
  }
306
304
  function renderMath(root) {
307
- if (DEBUG_MODE) log("execute math");
308
305
  const scope = root || document;
309
306
  const scripts = scope.querySelectorAll('script[type^="math/tex"]');
310
307
  scripts.forEach(function(script) {
@@ -323,53 +320,46 @@ class Body:
323
320
  if (parent) parent.replaceChild(element, script);
324
321
  });
325
322
  }
323
+
326
324
  function isNearBottom(marginPx = 100) {
327
- const el = document.scrollingElement || document.documentElement;
325
+ const el = SE;
328
326
  const distanceToBottom = el.scrollHeight - el.clientHeight - el.scrollTop;
329
327
  return distanceToBottom <= marginPx;
330
328
  }
331
329
  function scheduleScroll(live = false) {
332
- // Skip scheduling live auto-scroll when user disabled follow
333
330
  if (live === true && autoFollow !== true) return;
334
331
  if (scrollScheduled) return;
335
332
  scrollScheduled = true;
336
333
  requestAnimationFrame(function() {
337
334
  scrollScheduled = false;
338
335
  scrollToBottom(live);
339
- // keep FAB state in sync after any programmatic scroll
340
336
  scheduleScrollFabUpdate();
341
337
  });
342
338
  }
343
- // Force immediate scroll to bottom (pre-interaction bootstrap)
344
339
  function forceScrollToBottomImmediate() {
345
- const el = document.scrollingElement || document.documentElement;
346
- el.scrollTop = el.scrollHeight; // no behavior, no RAF, deterministic
340
+ const el = SE;
341
+ el.scrollTop = el.scrollHeight;
347
342
  prevScroll = el.scrollHeight;
348
343
  }
349
344
  function scrollToBottom(live = false, force = false) {
350
- const el = document.scrollingElement || document.documentElement;
345
+ const el = SE;
351
346
  const marginPx = 450;
352
347
  const behavior = (live === true) ? 'instant' : 'smooth';
353
-
354
- // Respect user-follow state during live updates
355
348
  if (live === true && autoFollow !== true) {
356
- // Keep prevScroll consistent for potential consumers
357
349
  prevScroll = el.scrollHeight;
358
350
  return;
359
351
  }
360
-
361
- // Allow initial auto-follow before any user interaction
362
352
  if ((live === true && userInteracted === false) || isNearBottom(marginPx) || live == false || force) {
363
353
  el.scrollTo({ top: el.scrollHeight, behavior });
364
354
  }
365
355
  prevScroll = el.scrollHeight;
366
356
  }
357
+
367
358
  function appendToInput(content) {
368
359
  userInteracted = false;
369
360
  const element = els.appendInput || document.getElementById('_append_input_');
370
361
  if (element) {
371
362
  element.insertAdjacentHTML('beforeend', content);
372
- highlightCode(true, element);
373
363
  scheduleScroll();
374
364
  }
375
365
  }
@@ -383,28 +373,49 @@ class Body:
383
373
  }
384
374
  return element;
385
375
  }
376
+ function getStreamMsg(create = true, name_header = '') {
377
+ const container = getStreamContainer();
378
+ if (!container) return null;
379
+ if (domStreamMsg && domStreamMsg.isConnected) return domStreamMsg;
380
+ let box = container.querySelector('.msg-box');
381
+ let msg = null;
382
+ if (!box && create) {
383
+ box = document.createElement('div');
384
+ box.classList.add('msg-box');
385
+ box.classList.add('msg-bot');
386
+ if (name_header != '') {
387
+ const name = document.createElement('div');
388
+ name.classList.add('name-header');
389
+ name.classList.add('name-bot');
390
+ name.innerHTML = name_header;
391
+ box.appendChild(name);
392
+ }
393
+ msg = document.createElement('div');
394
+ msg.classList.add('msg');
395
+ box.appendChild(msg);
396
+ container.appendChild(box);
397
+ } else if (box) {
398
+ msg = box.querySelector('.msg');
399
+ }
400
+ if (msg) domStreamMsg = msg;
401
+ return msg;
402
+ }
386
403
  function appendNode(content) {
387
404
  userInteracted = false;
388
- if (DEBUG_MODE) {
389
- log("APPEND NODE: {" + content + "}");
390
- }
391
405
  clearStreamBefore();
392
406
  prevScroll = 0;
393
407
  const element = els.nodes || document.getElementById('_nodes_');
394
408
  if (element) {
395
409
  element.classList.remove('empty_list');
396
410
  element.insertAdjacentHTML('beforeend', content);
397
- highlightCode(true, element);
398
- scrollToBottom(false); // without schedule
411
+ scheduleHighlight(element, true);
412
+ scrollToBottom(false);
399
413
  scheduleScrollFabUpdate();
400
414
  }
401
415
  clearHighlightCache();
402
416
  }
403
417
  function replaceNodes(content) {
404
418
  userInteracted = false;
405
- if (DEBUG_MODE) {
406
- log("REPLACE NODES: {" + content + "}");
407
- }
408
419
  clearStreamBefore();
409
420
  prevScroll = 0;
410
421
  const element = els.nodes || document.getElementById('_nodes_');
@@ -412,16 +423,13 @@ class Body:
412
423
  element.classList.remove('empty_list');
413
424
  element.replaceChildren();
414
425
  element.insertAdjacentHTML('beforeend', content);
415
- highlightCode(true, element);
416
- scrollToBottom(false, true); // without schedule
426
+ scheduleHighlight(element, true);
427
+ scrollToBottom(false, true);
417
428
  scheduleScrollFabUpdate();
418
429
  }
419
430
  clearHighlightCache();
420
431
  }
421
432
  function clean() {
422
- if (DEBUG_MODE) {
423
- log("-- CLEAN DOM --");
424
- }
425
433
  userInteracted = false;
426
434
  const el = els.nodes || document.getElementById('_nodes_');
427
435
  if (el) {
@@ -429,15 +437,6 @@ class Body:
429
437
  }
430
438
  resetEphemeralDomRefs();
431
439
  els = {};
432
- /*
433
- try {
434
- if (window.gc) {
435
- window.gc();
436
- }
437
- } catch (e) {
438
- // gc not available
439
- }
440
- */
441
440
  }
442
441
  function clearHighlightCache() {
443
442
  //
@@ -450,7 +449,7 @@ class Body:
450
449
  const extra = element.querySelector('.msg-extra');
451
450
  if (extra) {
452
451
  extra.insertAdjacentHTML('beforeend', content);
453
- highlightCode(true, extra);
452
+ scheduleHighlight(extra, true);
454
453
  scheduleScroll();
455
454
  }
456
455
  }
@@ -484,58 +483,33 @@ class Body:
484
483
  }
485
484
  });
486
485
  resetEphemeralDomRefs();
487
- highlightCode(true, container);
486
+ scheduleHighlight(container, true);
488
487
  scheduleScroll();
489
488
  }
490
489
  }
491
490
  function clearStream() {
492
491
  hideTips();
493
- if (DEBUG_MODE) {
494
- log("STREAM CLEAR");
495
- }
496
492
  domLastParagraphBlock = null;
497
493
  domLastCodeBlock = null;
494
+ domStreamMsg = null;
498
495
  domOutputStream = null;
499
- const element = getStreamContainer();
500
- if (element) {
501
- let box = element.querySelector('.msg-box');
502
- let msg;
503
- if (!box) {
504
- box = document.createElement('div');
505
- box.classList.add('msg-box');
506
- box.classList.add('msg-bot');
507
- msg = document.createElement('div');
508
- msg.classList.add('msg');
509
- box.appendChild(msg);
510
- element.appendChild(box);
511
- } else {
512
- msg = box.querySelector('.msg');
513
- }
514
- if (msg) {
515
- msg.replaceChildren();
516
- }
517
- }
496
+ el = getStreamContainer();
497
+ if (!el) return;
498
+ el.replaceChildren();
518
499
  }
519
500
  function beginStream() {
520
501
  hideTips();
521
- if (DEBUG_MODE) {
522
- log("STREAM BEGIN");
523
- }
524
502
  userInteracted = false;
525
503
  clearOutput();
526
- // Ensure initial auto-follow baseline before any chunks overflow
527
504
  forceScrollToBottomImmediate();
528
505
  scheduleScroll();
529
506
  }
530
507
  function endStream() {
531
- if (DEBUG_MODE) {
532
- log("STREAM END");
533
- }
534
508
  clearOutput();
535
509
  bridgeReconnect();
536
510
  }
511
+
537
512
  function enqueueStream(name_header, content, chunk, replace = false, is_code_block = false) {
538
- // Push incoming chunk; scheduling is done with RAF to batch DOM ops
539
513
  streamQ.push({name_header, content, chunk, replace, is_code_block});
540
514
  if (!streamRAF) {
541
515
  streamRAF = requestAnimationFrame(drainStream);
@@ -544,139 +518,112 @@ class Body:
544
518
  function drainStream() {
545
519
  streamRAF = 0;
546
520
  let processed = 0;
547
-
548
- // Emergency coalescing if queue grows too large
549
521
  const shouldAggressiveCoalesce = streamQ.length >= STREAM_EMERGENCY_COALESCE_LEN;
550
-
522
+
523
+ batching = true; // start partii
551
524
  while (streamQ.length && processed < STREAM_MAX_PER_FRAME) {
552
525
  let {name_header, content, chunk, replace, is_code_block} = streamQ.shift();
553
-
554
- // Coalesce contiguous simple appends to reduce DOM churn
555
526
  if (!replace && !content && (chunk && chunk.length > 0)) {
556
- // Collect chunks into an array to avoid O(n^2) string concatenation
557
527
  const chunks = [chunk];
558
528
  while (streamQ.length) {
559
529
  const next = streamQ[0];
560
530
  if (!next.replace && !next.content && next.is_code_block === is_code_block && next.name_header === name_header) {
561
531
  chunks.push(next.chunk);
562
532
  streamQ.shift();
563
- if (!shouldAggressiveCoalesce) {
564
- // Light coalescing per frame is enough under normal conditions
565
- break;
566
- }
567
- } else {
568
- break;
569
- }
533
+ if (!shouldAggressiveCoalesce) break;
534
+ } else break;
570
535
  }
571
536
  chunk = chunks.join('');
572
537
  }
573
-
574
538
  applyStream(name_header, content, chunk, replace, is_code_block);
575
539
  processed++;
576
540
  }
577
-
578
- // If there are remaining items re-schedule next frame
541
+ batching = false; // koniec partii
542
+
543
+ // jedno przewinięcie na partię
544
+ if (needScroll) {
545
+ if (userInteracted === false) forceScrollToBottomImmediate();
546
+ else scheduleScroll(true);
547
+ needScroll = false;
548
+ }
549
+
579
550
  if (streamQ.length) {
580
551
  streamRAF = requestAnimationFrame(drainStream);
581
552
  }
582
553
  }
583
- // Public API: enqueue and process in the next animation frame
584
554
  function appendStream(name_header, content, chunk, replace = false, is_code_block = false) {
585
555
  enqueueStream(name_header, content, chunk, replace, is_code_block);
586
556
  }
587
- // Internal: performs actual DOM updates for a single merged chunk
588
557
  function applyStream(name_header, content, chunk, replace = false, is_code_block = false) {
589
- dropIfDetached(); // clear references to detached elements
558
+ dropIfDetached();
590
559
  hideTips();
591
- if (DEBUG_MODE) {
592
- log("APPLY CHUNK: {" + chunk + "}, CONTENT: {"+content+"}, replace: " + replace + ", is_code_block: " + is_code_block);
593
- }
594
- const element = getStreamContainer();
595
- let msg;
596
- if (element) {
597
- let box = element.querySelector('.msg-box');
598
- if (!box) {
599
- box = document.createElement('div');
600
- box.classList.add('msg-box');
601
- box.classList.add('msg-bot');
602
- if (name_header != '') {
603
- const name = document.createElement('div');
604
- name.classList.add('name-header');
605
- name.classList.add('name-bot');
606
- name.innerHTML = name_header;
607
- box.appendChild(name);
560
+ const msg = getStreamMsg(true, name_header);
561
+ if (msg) {
562
+ if (replace) {
563
+ domLastCodeBlock = null;
564
+ domLastParagraphBlock = null;
565
+ msg.replaceChildren();
566
+ if (content) {
567
+ msg.insertAdjacentHTML('afterbegin', content);
568
+ }
569
+ let doMath = true;
570
+ if (is_code_block) {
571
+ doMath = false;
608
572
  }
609
- msg = document.createElement('div');
610
- msg.classList.add('msg');
611
- box.appendChild(msg);
612
- element.appendChild(box);
573
+ highlightCode(doMath, msg);
613
574
  } else {
614
- msg = box.querySelector('.msg');
615
- }
616
- if (msg) {
617
- if (replace) {
618
- domLastCodeBlock = null;
619
- domLastParagraphBlock = null;
620
- msg.replaceChildren();
621
- if (content) {
622
- msg.insertAdjacentHTML('afterbegin', content);
623
- }
624
- let doMath = true;
625
- if (is_code_block) {
626
- doMath = false;
627
- }
628
- highlightCode(doMath, msg);
629
- } else {
630
- if (is_code_block) {
631
- // Try to reuse cached last code block; fallback to cheap lastElementChild check
632
- let lastCodeBlock = domLastCodeBlock;
633
- if (!lastCodeBlock || !msg.contains(lastCodeBlock)) {
634
- const last = msg.lastElementChild;
635
- if (last && last.tagName === 'PRE') {
636
- const codeEl = last.querySelector('code');
637
- if (codeEl) {
638
- lastCodeBlock = codeEl;
639
- }
640
- } else {
641
- // Fallback scan only when necessary
642
- const codes = msg.querySelectorAll('pre code');
643
- if (codes.length > 0) {
644
- lastCodeBlock = codes[codes.length - 1];
645
- }
575
+ if (is_code_block) {
576
+ let lastCodeBlock = domLastCodeBlock;
577
+ if (!lastCodeBlock || !msg.contains(lastCodeBlock)) {
578
+ const last = msg.lastElementChild;
579
+ if (last && last.tagName === 'PRE') {
580
+ const codeEl = last.querySelector('code');
581
+ if (codeEl) {
582
+ lastCodeBlock = codeEl;
646
583
  }
647
- }
648
- if (lastCodeBlock) {
649
- lastCodeBlock.insertAdjacentHTML('beforeend', chunk);
650
- domLastCodeBlock = lastCodeBlock;
651
584
  } else {
652
- msg.insertAdjacentHTML('beforeend', chunk);
653
- domLastCodeBlock = null;
585
+ const codes = msg.querySelectorAll('pre code');
586
+ if (codes.length > 0) {
587
+ lastCodeBlock = codes[codes.length - 1];
588
+ }
654
589
  }
590
+ }
591
+ if (lastCodeBlock) {
592
+ lastCodeBlock.insertAdjacentHTML('beforeend', chunk);
593
+ domLastCodeBlock = lastCodeBlock;
655
594
  } else {
595
+ msg.insertAdjacentHTML('beforeend', chunk);
656
596
  domLastCodeBlock = null;
657
- let p = (domLastParagraphBlock && msg.contains(domLastParagraphBlock))
658
- ? domLastParagraphBlock
659
- : (msg.lastElementChild && msg.lastElementChild.tagName === 'P'
660
- ? msg.lastElementChild
661
- : null);
662
- if (p) {
663
- p.insertAdjacentHTML('beforeend', chunk);
664
- domLastParagraphBlock = p;
665
- } else {
666
- msg.insertAdjacentHTML('beforeend', chunk);
667
- const last = msg.lastElementChild;
668
- domLastParagraphBlock = (last && last.tagName === 'P') ? last : null;
669
- }
597
+ }
598
+ } else {
599
+ domLastCodeBlock = null;
600
+ let p = (domLastParagraphBlock && msg.contains(domLastParagraphBlock))
601
+ ? domLastParagraphBlock
602
+ : (msg.lastElementChild && msg.lastElementChild.tagName === 'P'
603
+ ? msg.lastElementChild
604
+ : null);
605
+ if (p) {
606
+ p.insertAdjacentHTML('beforeend', chunk);
607
+ domLastParagraphBlock = p;
608
+ } else {
609
+ msg.insertAdjacentHTML('beforeend', chunk);
610
+ const last = msg.lastElementChild;
611
+ domLastParagraphBlock = (last && last.tagName === 'P') ? last : null;
670
612
  }
671
613
  }
672
614
  }
673
615
  }
674
- // Initial auto-follow until first user interaction
675
- if (userInteracted === false) {
676
- forceScrollToBottomImmediate();
677
- } else {
678
- scheduleScroll(true);
679
- }
616
+ if (batching) {
617
+ needScroll = true;
618
+ } else {
619
+ if (userInteracted === false) {
620
+ forceScrollToBottomImmediate();
621
+ } else {
622
+ scheduleScroll(true);
623
+ }
624
+ }
625
+ fabFreezeUntil = performance.now() + FAB_TOGGLE_DEBOUNCE_MS;
626
+ scheduleScrollFabUpdate();
680
627
  }
681
628
  function nextStream() {
682
629
  hideTips();
@@ -690,6 +637,7 @@ class Body:
690
637
  elementBefore.appendChild(frag);
691
638
  domLastCodeBlock = null;
692
639
  domLastParagraphBlock = null;
640
+ domStreamMsg = null;
693
641
  scheduleScroll();
694
642
  }
695
643
  }
@@ -798,7 +746,7 @@ class Body:
798
746
  element.classList.add('visible');
799
747
  }
800
748
  element.innerHTML = content;
801
- highlightCode(true, element);
749
+ scheduleHighlight(element, true);
802
750
  scheduleScroll();
803
751
  }
804
752
  }
@@ -828,7 +776,8 @@ class Body:
828
776
  clearStreamBefore();
829
777
  domLastCodeBlock = null;
830
778
  domLastParagraphBlock = null;
831
- domOutputStream = null; // release handle to allow GC on old container subtree
779
+ domStreamMsg = null;
780
+ domOutputStream = null;
832
781
  const element = els.appendOutput || document.getElementById('_append_output_');
833
782
  if (element) {
834
783
  element.replaceChildren();
@@ -906,7 +855,7 @@ class Body:
906
855
  const source = wrapper.querySelector('code');
907
856
  if (source && collapsed_idx.includes(index)) {
908
857
  source.style.display = 'none';
909
- const collapseBtn = wrapper.querySelector('code-header-collapse');
858
+ const collapseBtn = wrapper.querySelector('.code-header-collapse');
910
859
  if (collapseBtn) {
911
860
  const collapseSpan = collapseBtn.querySelector('span');
912
861
  if (collapseSpan) {
@@ -974,112 +923,112 @@ class Body:
974
923
  }
975
924
  }
976
925
 
977
- // ---------- scroll bottom ----------
978
926
  function hasVerticalScroll() {
979
- const el = document.scrollingElement || document.documentElement;
927
+ const el = SE;
980
928
  return (el.scrollHeight - el.clientHeight) > 1;
981
929
  }
982
930
  function distanceToBottomPx() {
983
- const el = document.scrollingElement || document.documentElement;
931
+ const el = SE;
984
932
  return el.scrollHeight - el.clientHeight - el.scrollTop;
985
933
  }
986
934
  function isAtBottom(thresholdPx = 2) {
987
935
  return distanceToBottomPx() <= thresholdPx;
988
936
  }
989
- function isLoaderHidden() {
990
- const el = els.loader || document.getElementById('_loader_');
991
- // If loader element is missing treat as hidden to avoid blocking FAB unnecessarily
992
- return !el || el.classList.contains('hidden');
937
+
938
+ function computeFabAction() {
939
+ const el = SE;
940
+ const hasScroll = (el.scrollHeight - el.clientHeight) > 1;
941
+ if (!hasScroll) return 'none';
942
+ const dist = el.scrollHeight - el.clientHeight - el.scrollTop;
943
+ if (dist <= 2) return 'up';
944
+ if (dist >= SHOW_DOWN_THRESHOLD_PX) return 'down';
945
+ return 'none';
993
946
  }
994
- function updateScrollFab() {
947
+ function updateScrollFab(force = false, actionOverride = null, bypassFreeze = false) {
995
948
  const btn = els.scrollFab || document.getElementById('scrollFab');
996
949
  const icon = els.scrollFabIcon || document.getElementById('scrollFabIcon');
997
950
  if (!btn || !icon) return;
998
951
 
999
- const hasScroll = hasVerticalScroll();
1000
- if (!hasScroll) {
1001
- btn.classList.remove('visible');
1002
- currentFabAction = 'none';
952
+ const action = actionOverride || computeFabAction();
953
+ if (!force && !bypassFreeze && performance.now() < fabFreezeUntil && action !== currentFabAction) {
1003
954
  return;
1004
955
  }
1005
-
1006
- const atBottom = isAtBottom();
1007
- const dist = distanceToBottomPx();
1008
- const loaderHidden = isLoaderHidden();
1009
-
1010
- // Determine desired action and visibility based on requirements:
1011
- // - Show "down" only when at least SHOW_DOWN_THRESHOLD_PX away from bottom.
1012
- // - Show "up" only when loader-global has the 'hidden' class.
1013
- // - Otherwise hide the FAB to prevent overlap and noise.
1014
- let action = 'none'; // 'up' | 'down' | 'none'
1015
- if (atBottom) {
1016
- action = 'up';
1017
- } else {
1018
- if (dist >= SHOW_DOWN_THRESHOLD_PX) {
1019
- action = 'down';
1020
- } else {
1021
- action = 'none';
1022
- }
1023
- }
1024
-
1025
956
  if (action === 'none') {
1026
- btn.classList.remove('visible');
1027
- currentFabAction = 'none';
957
+ if (currentFabAction !== 'none' || force) {
958
+ btn.classList.remove('visible');
959
+ currentFabAction = 'none';
960
+ }
1028
961
  return;
1029
962
  }
1030
-
1031
- // Update icon and semantics only if changed to avoid redundant 'load' events
1032
- if (action !== currentFabAction) {
963
+ if (action !== currentFabAction || force) {
1033
964
  if (action === 'up') {
1034
- if (icon.src !== ICON_COLLAPSE) icon.src = ICON_COLLAPSE;
965
+ if (icon.dataset.dir !== 'up') {
966
+ icon.src = ICON_COLLAPSE;
967
+ icon.dataset.dir = 'up';
968
+ }
1035
969
  btn.title = "Go to top";
1036
970
  } else {
1037
- if (icon.src !== ICON_EXPAND) icon.src = ICON_EXPAND;
971
+ if (icon.dataset.dir !== 'down') {
972
+ icon.src = ICON_EXPAND;
973
+ icon.dataset.dir = 'down';
974
+ }
1038
975
  btn.title = "Go to bottom";
1039
976
  }
1040
977
  btn.setAttribute('aria-label', btn.title);
1041
978
  currentFabAction = action;
979
+ btn.classList.add('visible');
980
+ } else if (!btn.classList.contains('visible')) {
981
+ btn.classList.add('visible');
1042
982
  }
1043
-
1044
- // Finally show
1045
- btn.classList.add('visible');
1046
983
  }
1047
984
  function scheduleScrollFabUpdate() {
1048
985
  if (scrollFabUpdateScheduled) return;
1049
986
  scrollFabUpdateScheduled = true;
1050
987
  requestAnimationFrame(function() {
1051
988
  scrollFabUpdateScheduled = false;
1052
- updateScrollFab();
989
+ const action = computeFabAction();
990
+ if (action !== currentFabAction) {
991
+ updateScrollFab(false, action);
992
+ }
1053
993
  });
1054
994
  }
995
+
996
+ function maybeEnableAutoFollowByProximity() {
997
+ const el = SE;
998
+ if (!autoFollow) {
999
+ const distanceToBottom = el.scrollHeight - el.clientHeight - el.scrollTop;
1000
+ if (distanceToBottom <= AUTO_FOLLOW_REENABLE_PX) {
1001
+ autoFollow = true;
1002
+ }
1003
+ }
1004
+ }
1055
1005
  function scrollToTopUser() {
1056
- // Explicit user-driven scroll disables auto-follow
1057
1006
  userInteracted = true;
1058
1007
  autoFollow = false;
1059
1008
  try {
1060
- const el = document.scrollingElement || document.documentElement;
1009
+ const el = SE;
1061
1010
  el.scrollTo({ top: 0, behavior: 'smooth' });
1011
+ lastScrollTop = el.scrollTop;
1062
1012
  } catch (e) {
1063
- // Fallback in environments without smooth scrolling support
1064
- const el = document.scrollingElement || document.documentElement;
1013
+ const el = SE;
1065
1014
  el.scrollTop = 0;
1015
+ lastScrollTop = 0;
1066
1016
  }
1067
- scheduleScrollFabUpdate();
1068
1017
  }
1069
1018
  function scrollToBottomUser() {
1070
- // User action to go to bottom re-enables auto-follow
1071
1019
  userInteracted = true;
1072
- autoFollow = true;
1020
+ autoFollow = false;
1073
1021
  try {
1074
- const el = document.scrollingElement || document.documentElement;
1022
+ const el = SE;
1075
1023
  el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
1024
+ lastScrollTop = el.scrollTop;
1076
1025
  } catch (e) {
1077
- const el = document.scrollingElement || document.documentElement;
1026
+ const el = SE;
1078
1027
  el.scrollTop = el.scrollHeight;
1028
+ lastScrollTop = el.scrollTop;
1079
1029
  }
1080
- scheduleScrollFabUpdate();
1030
+ maybeEnableAutoFollowByProximity();
1081
1031
  }
1082
- // ---------- end of scroll bottom ----------
1083
1032
 
1084
1033
  document.addEventListener('DOMContentLoaded', function() {
1085
1034
  new QWebChannel(qt.webChannelTransport, function (channel) {
@@ -1089,6 +1038,7 @@ class Body:
1089
1038
  });
1090
1039
  initDomRefs();
1091
1040
  const container = els.container;
1041
+
1092
1042
  function addClassToMsg(id, className) {
1093
1043
  const msgElement = document.getElementById('msg-bot-' + id);
1094
1044
  if (msgElement) {
@@ -1101,61 +1051,71 @@ class Body:
1101
1051
  msgElement.classList.remove(className);
1102
1052
  }
1103
1053
  }
1104
- container.addEventListener('mouseover', function(event) {
1054
+ containerMouseOverHandler = function(event) {
1105
1055
  if (event.target.classList.contains('action-img')) {
1106
1056
  const id = event.target.getAttribute('data-id');
1107
1057
  addClassToMsg(id, 'msg-highlight');
1108
1058
  }
1109
- });
1110
- container.addEventListener('mouseout', function(event) {
1059
+ };
1060
+ containerMouseOutHandler = function(event) {
1111
1061
  if (event.target.classList.contains('action-img')) {
1112
1062
  const id = event.target.getAttribute('data-id');
1113
1063
  removeClassFromMsg(id, 'msg-highlight');
1114
1064
  }
1115
- });
1116
- // Wheel up disables auto-follow immediately (works even at absolute bottom)
1117
- document.addEventListener('wheel', function(ev) {
1065
+ };
1066
+ container.addEventListener('mouseover', containerMouseOverHandler, { passive: true });
1067
+ container.addEventListener('mouseout', containerMouseOutHandler, { passive: true });
1068
+
1069
+ wheelHandler = function(ev) {
1118
1070
  userInteracted = true;
1119
1071
  if (ev.deltaY < 0) {
1120
1072
  autoFollow = false;
1073
+ } else {
1074
+ maybeEnableAutoFollowByProximity();
1121
1075
  }
1122
- }, { passive: true });
1076
+ };
1077
+ document.addEventListener('wheel', wheelHandler, { passive: true });
1123
1078
 
1124
- // Track scroll direction and restore auto-follow when user returns to bottom
1125
- window.addEventListener('scroll', function() {
1126
- const el = document.scrollingElement || document.documentElement;
1079
+ scrollHandler = function() {
1080
+ const el = SE;
1127
1081
  const top = el.scrollTop;
1128
-
1129
- // User scrolled up (ignore tiny jitter)
1130
1082
  if (top + 1 < lastScrollTop) {
1131
1083
  autoFollow = false;
1132
- } else if (!autoFollow) {
1133
- const distanceToBottom = el.scrollHeight - el.clientHeight - top;
1134
- if (distanceToBottom <= AUTO_FOLLOW_REENABLE_PX) {
1135
- autoFollow = true;
1136
- }
1137
1084
  }
1085
+ maybeEnableAutoFollowByProximity();
1138
1086
  lastScrollTop = top;
1139
- }, { passive: true });
1087
+ const action = computeFabAction();
1088
+ if (action !== currentFabAction) {
1089
+ updateScrollFab(false, action, true);
1090
+ }
1091
+ };
1092
+ window.addEventListener('scroll', scrollHandler, { passive: true });
1140
1093
 
1141
- // Scroll-to-top/bottom FAB wiring
1142
1094
  if (els.scrollFab) {
1143
- els.scrollFab.addEventListener('click', function(ev) {
1095
+ fabClickHandler = function(ev) {
1144
1096
  ev.preventDefault();
1145
- if (isAtBottom()) {
1097
+ ev.stopPropagation();
1098
+ const action = computeFabAction();
1099
+ if (action === 'up') {
1146
1100
  scrollToTopUser();
1147
- } else {
1101
+ } else if (action === 'down') {
1148
1102
  scrollToBottomUser();
1149
1103
  }
1150
- }, { passive: false });
1104
+ fabFreezeUntil = performance.now() + FAB_TOGGLE_DEBOUNCE_MS;
1105
+ updateScrollFab(true);
1106
+ };
1107
+ els.scrollFab.addEventListener('click', fabClickHandler, { passive: false });
1151
1108
  }
1152
- window.addEventListener('scroll', scheduleScrollFabUpdate, { passive: true });
1153
- window.addEventListener('resize', scheduleScrollFabUpdate, { passive: true });
1154
1109
 
1155
- // Initial state
1156
- scheduleScrollFabUpdate();
1110
+ resizeHandler = function() {
1111
+ maybeEnableAutoFollowByProximity();
1112
+ scheduleScrollFabUpdate();
1113
+ };
1114
+ window.addEventListener('resize', resizeHandler, { passive: true });
1115
+
1116
+ updateScrollFab(true);
1157
1117
 
1158
- container.addEventListener('click', function(event) {
1118
+ containerClickHandler = function(event) {
1159
1119
  const copyButton = event.target.closest('.code-header-copy');
1160
1120
  if (copyButton) {
1161
1121
  event.preventDefault();
@@ -1228,13 +1188,13 @@ class Body:
1228
1188
  }
1229
1189
  }
1230
1190
  }
1231
- });
1191
+ };
1192
+ container.addEventListener('click', containerClickHandler, { passive: false });
1232
1193
 
1233
- // Cleanup on page lifecycle changes
1234
- window.addEventListener('pagehide', teardown, { passive: true });
1235
- window.addEventListener('beforeunload', teardown, { passive: true });
1194
+ // window.addEventListener('pagehide', cleanup, { once: true });
1195
+ // window.addEventListener('beforeunload', cleanup, { once: true });
1236
1196
  });
1237
- setTimeout(cycleTips, 10000); // after 10 seconds
1197
+ setTimeout(cycleTips, 10000);
1238
1198
  </script>
1239
1199
  </head>
1240
1200
  <body """
@@ -1304,7 +1264,6 @@ class Body:
1304
1264
  }
1305
1265
  """
1306
1266
 
1307
- # CSS for the scroll-to-top/bottom
1308
1267
  _SCROLL_FAB_CSS = """
1309
1268
  #scrollFab.scroll-fab {
1310
1269
  position: fixed;
@@ -1371,11 +1330,6 @@ class Body:
1371
1330
  """
1372
1331
 
1373
1332
  def __init__(self, window=None):
1374
- """
1375
- HTML Body
1376
-
1377
- :param window: Window instance
1378
- """
1379
1333
  self.window = window
1380
1334
  self.highlight = SyntaxHighlight(window)
1381
1335
  self._tip_keys = tuple(f"output.tips.{i}" for i in range(1, self.NUM_TIPS + 1))
@@ -1399,21 +1353,10 @@ class Body:
1399
1353
  "zenburn",
1400
1354
  )
1401
1355
 
1402
-
1403
1356
  def is_timestamp_enabled(self) -> bool:
1404
- """
1405
- Check if timestamp is enabled
1406
-
1407
- :return: True if timestamp is enabled
1408
- """
1409
1357
  return self.window.core.config.get('output_timestamp')
1410
1358
 
1411
1359
  def prepare_styles(self) -> str:
1412
- """
1413
- Prepare CSS styles
1414
-
1415
- :return: CSS styles
1416
- """
1417
1360
  cfg = self.window.core.config
1418
1361
  fonts_path = os.path.join(cfg.get_app_path(), "data", "fonts").replace("\\", "/")
1419
1362
  syntax_style = self.window.core.config.get("render.code_syntax") or "default"
@@ -1424,30 +1367,17 @@ class Body:
1424
1367
  theme_css,
1425
1368
  "pre { color: #fff; }" if syntax_style in self._syntax_dark else "pre { color: #000; }",
1426
1369
  self.highlight.get_style_defs(),
1427
- self._PERFORMANCE_CSS # performance improvements
1370
+ self._PERFORMANCE_CSS
1428
1371
  ]
1429
1372
  return "\n".join(parts)
1430
1373
 
1431
1374
  def prepare_action_icons(self, ctx: CtxItem) -> str:
1432
- """
1433
- Append action icons
1434
-
1435
- :param ctx: context item
1436
- :return: HTML code
1437
- """
1438
1375
  icons_html = "".join(self.get_action_icons(ctx, all=True))
1439
1376
  if icons_html:
1440
1377
  return f'<div class="action-icons" data-id="{ctx.id}">{icons_html}</div>'
1441
1378
  return ""
1442
1379
 
1443
1380
  def get_action_icons(self, ctx: CtxItem, all: bool = False) -> List[str]:
1444
- """
1445
- Get action icons for context item
1446
-
1447
- :param ctx: context item
1448
- :param all: True to show all icons
1449
- :return: list of icons
1450
- """
1451
1381
  icons: List[str] = []
1452
1382
  if ctx.output:
1453
1383
  cid = ctx.id
@@ -1467,46 +1397,17 @@ class Body:
1467
1397
  f'<a href="extra-join:{cid}" class="action-icon edit-icon" data-id="{cid}" role="button"><span class="cmd">{self.get_icon("playlist_add", t("ctx.extra.join"), ctx)}</span></a>')
1468
1398
  return icons
1469
1399
 
1470
- def get_icon(
1471
- self,
1472
- icon: str,
1473
- title: Optional[str] = None,
1474
- item: Optional[CtxItem] = None
1475
- ) -> str:
1476
- """
1477
- Get icon
1478
-
1479
- :param icon: icon name
1480
- :param title: icon title
1481
- :param item: context item
1482
- :return: icon HTML
1483
- """
1400
+ def get_icon(self, icon: str, title: Optional[str] = None, item: Optional[CtxItem] = None) -> str:
1484
1401
  app_path = self.window.core.config.get_app_path()
1485
1402
  icon_path = os.path.join(app_path, "data", "icons", f"{icon}.svg")
1486
1403
  return f'<img src="file://{icon_path}" class="action-img" title="{title}" alt="{title}" data-id="{item.id}">'
1487
1404
 
1488
- def get_image_html(
1489
- self,
1490
- url: str,
1491
- num: Optional[int] = None,
1492
- num_all: Optional[int] = None
1493
- ) -> str:
1494
- """
1495
- Get media image/video/audio HTML
1496
-
1497
- :param url: URL to image
1498
- :param num: number of image
1499
- :param num_all: number of all images
1500
- :return: HTML code
1501
- """
1405
+ def get_image_html(self, url: str, num: Optional[int] = None, num_all: Optional[int] = None) -> str:
1502
1406
  url, path = self.window.core.filesystem.extract_local_url(url)
1503
1407
  basename = os.path.basename(path)
1504
-
1505
- # if video file then embed video player
1506
1408
  ext = os.path.splitext(basename)[1].lower()
1507
1409
  video_exts = (".mp4", ".webm", ".ogg", ".mov", ".avi", ".mkv")
1508
1410
  if ext in video_exts:
1509
- # check if .webm file exists for better compatibility
1510
1411
  if ext != ".webm":
1511
1412
  webm_path = os.path.splitext(path)[0] + ".webm"
1512
1413
  if os.path.exists(webm_path):
@@ -1522,20 +1423,7 @@ class Body:
1522
1423
  '''
1523
1424
  return f'<div class="extra-src-img-box" title="{url}"><div class="img-outer"><div class="img-wrapper"><a href="{url}"><img src="{path}" class="image"></a></div><a href="{url}" class="title">{elide_filename(basename)}</a></div></div><br/>'
1524
1425
 
1525
- def get_url_html(
1526
- self,
1527
- url: str,
1528
- num: Optional[int] = None,
1529
- num_all: Optional[int] = None
1530
- ) -> str:
1531
- """
1532
- Get URL HTML
1533
-
1534
- :param url: external URL
1535
- :param num: number of URL
1536
- :param num_all: number of all URLs
1537
- :return: HTML code
1538
- """
1426
+ def get_url_html(self, url: str, num: Optional[int] = None, num_all: Optional[int] = None) -> str:
1539
1427
  app_path = self.window.core.config.get_app_path()
1540
1428
  icon_path = os.path.join(app_path, "data", "icons", "language.svg").replace("\\", "/")
1541
1429
  icon = f'<img src="file://{icon_path}" class="extra-src-icon">'
@@ -1543,12 +1431,6 @@ class Body:
1543
1431
  return f'{icon}<a href="{url}" title="{url}">{url}</a> <small>{num_str}</small>'
1544
1432
 
1545
1433
  def get_docs_html(self, docs: List[Dict]) -> str:
1546
- """
1547
- Get Llama-index doc metadata HTML
1548
-
1549
- :param docs: list of document metadata
1550
- :return: HTML code
1551
- """
1552
1434
  html_parts: List[str] = []
1553
1435
  src_parts: List[str] = []
1554
1436
  num = 1
@@ -1578,20 +1460,7 @@ class Body:
1578
1460
 
1579
1461
  return "".join(html_parts)
1580
1462
 
1581
- def get_file_html(
1582
- self,
1583
- url: str,
1584
- num: Optional[int] = None,
1585
- num_all: Optional[int] = None
1586
- ) -> str:
1587
- """
1588
- Get file HTML
1589
-
1590
- :param url: URL to file
1591
- :param num: number of file
1592
- :param num_all: number of all files
1593
- :return: HTML code
1594
- """
1463
+ def get_file_html(self, url: str, num: Optional[int] = None, num_all: Optional[int] = None) -> str:
1595
1464
  app_path = self.window.core.config.get_app_path()
1596
1465
  icon_path = os.path.join(app_path, "data", "icons", "attachments.svg").replace("\\", "/")
1597
1466
  icon = f'<img src="file://{icon_path}" class="extra-src-icon">'
@@ -1600,12 +1469,6 @@ class Body:
1600
1469
  return f'{icon} <b>{num_str}</b> <a href="{url}">{path}</a>'
1601
1470
 
1602
1471
  def prepare_tool_extra(self, ctx: CtxItem) -> str:
1603
- """
1604
- Prepare footer extra
1605
-
1606
- :param ctx: context item
1607
- :return: HTML code
1608
- """
1609
1472
  extra = ctx.extra
1610
1473
  if not extra:
1611
1474
  return ""
@@ -1643,11 +1506,6 @@ class Body:
1643
1506
  return "".join(parts)
1644
1507
 
1645
1508
  def get_all_tips(self) -> str:
1646
- """
1647
- Get all tips for the output view
1648
-
1649
- :return: JSON string of tips
1650
- """
1651
1509
  if not self.window.core.config.get("layout.tooltips", False):
1652
1510
  return "[]"
1653
1511
 
@@ -1661,12 +1519,6 @@ class Body:
1661
1519
  return _json_dumps(tips)
1662
1520
 
1663
1521
  def get_html(self, pid: int) -> str:
1664
- """
1665
- Build webview HTML code (fast path, minimal allocations)
1666
-
1667
- :param pid: process ID
1668
- :return: HTML code
1669
- """
1670
1522
  cfg_get = self.window.core.config.get
1671
1523
  style = cfg_get("theme.style", "blocks")
1672
1524
  classes = ["theme-" + style]
@@ -1680,7 +1532,6 @@ class Body:
1680
1532
  styles_css = self.prepare_styles()
1681
1533
  tips_json = self.get_all_tips()
1682
1534
 
1683
- # Build file:// paths for FAB icons
1684
1535
  app_path = self.window.core.config.get_app_path().replace("\\", "/")
1685
1536
  expand_path = os.path.join(app_path, "data", "icons", "expand.svg").replace("\\", "/")
1686
1537
  collapse_path = os.path.join(app_path, "data", "icons", "collapse.svg").replace("\\", "/")