pygpt-net 2.6.47__py3-none-any.whl → 2.6.48__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.
pygpt_net/CHANGELOG.txt CHANGED
@@ -1,3 +1,7 @@
1
+ 2.6.48 (2025-09-15)
2
+
3
+ - Added: auto-loading of next items to the list of contexts when scrolling to the end of the list.
4
+
1
5
  2.6.47 (2025-09-15)
2
6
 
3
7
  - Improved: Parsing of custom markup tags.
pygpt_net/__init__.py CHANGED
@@ -13,7 +13,7 @@ __author__ = "Marcin Szczygliński"
13
13
  __copyright__ = "Copyright 2025, Marcin Szczygliński"
14
14
  __credits__ = ["Marcin Szczygliński"]
15
15
  __license__ = "MIT"
16
- __version__ = "2.6.47"
16
+ __version__ = "2.6.48"
17
17
  __build__ = "2025-09-15"
18
18
  __maintainer__ = "Marcin Szczygliński"
19
19
  __github__ = "https://github.com/szczyglis-dev/py-gpt"
pygpt_net/app_core.py CHANGED
@@ -6,48 +6,48 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.01 23:00:00 #
9
+ # Updated Date: 2025.09.15 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from pygpt_net.config import Config
13
- from pygpt_net.core.access import Access
14
- from pygpt_net.core.agents import Agents
15
- from pygpt_net.core.assistants import Assistants
16
- from pygpt_net.core.attachments import Attachments
17
- from pygpt_net.core.audio import Audio
18
- from pygpt_net.core.bridge import Bridge
19
- from pygpt_net.core.calendar import Calendar
20
- from pygpt_net.core.camera import Camera
21
- # from pygpt_net.core.chain import Chain
22
- from pygpt_net.core.command import Command
23
- from pygpt_net.core.ctx import Ctx
24
- from pygpt_net.core.db import Database
25
- from pygpt_net.core.debug import Debug
26
- from pygpt_net.core.dispatcher import Dispatcher
27
- from pygpt_net.core.experts import Experts
28
- from pygpt_net.core.idx import Idx
29
- from pygpt_net.core.installer import Installer
30
- from pygpt_net.core.filesystem import Filesystem
31
- from pygpt_net.core.history import History
32
- from pygpt_net.core.image import Image
33
- from pygpt_net.core.llm import LLM
34
- from pygpt_net.core.models import Models
35
- from pygpt_net.core.modes import Modes
36
- from pygpt_net.core.notepad import Notepad
37
- from pygpt_net.core.platforms import Platforms
38
- from pygpt_net.core.plugins import Plugins
39
- from pygpt_net.core.presets import Presets
40
- from pygpt_net.core.prompt import Prompt
41
- from pygpt_net.core.settings import Settings
42
- from pygpt_net.core.tabs import Tabs
43
- from pygpt_net.core.text import Text
44
- from pygpt_net.core.tokens import Tokens
45
- from pygpt_net.core.updater import Updater
46
- from pygpt_net.core.video import Video
47
- from pygpt_net.core.vision import Vision
48
- from pygpt_net.core.web import Web
12
+ from .config import Config
13
+ from .core.access import Access
14
+ from .core.agents import Agents
15
+ from .core.assistants import Assistants
16
+ from .core.attachments import Attachments
17
+ from .core.audio import Audio
18
+ from .core.bridge import Bridge
19
+ from .core.calendar import Calendar
20
+ from .core.camera import Camera
21
+ # from .core.chain import Chain
22
+ from .core.command import Command
23
+ from .core.ctx import Ctx
24
+ from .core.db import Database
25
+ from .core.debug import Debug
26
+ from .core.dispatcher import Dispatcher
27
+ from .core.experts import Experts
28
+ from .core.idx import Idx
29
+ from .core.installer import Installer
30
+ from .core.filesystem import Filesystem
31
+ from .core.history import History
32
+ from .core.image import Image
33
+ from .core.llm import LLM
34
+ from .core.models import Models
35
+ from .core.modes import Modes
36
+ from .core.notepad import Notepad
37
+ from .core.platforms import Platforms
38
+ from .core.plugins import Plugins
39
+ from .core.presets import Presets
40
+ from .core.prompt import Prompt
41
+ from .core.settings import Settings
42
+ from .core.tabs import Tabs
43
+ from .core.text import Text
44
+ from .core.tokens import Tokens
45
+ from .core.updater import Updater
46
+ from .core.video import Video
47
+ from .core.vision import Vision
48
+ from .core.web import Web
49
49
 
50
- from pygpt_net.provider.api import Api
50
+ from .provider.api import Api
51
51
 
52
52
  class Core:
53
53
  def __init__(self, window=None):
@@ -6,42 +6,43 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.01 23:00:00 #
9
+ # Updated Date: 2025.09.15 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from pygpt_net.controller.access import Access
13
- from pygpt_net.controller.agent import Agent
14
- from pygpt_net.controller.assistant import Assistant
15
- from pygpt_net.controller.attachment import Attachment
16
- from pygpt_net.controller.audio import Audio
17
- from pygpt_net.controller.calendar import Calendar
18
- from pygpt_net.controller.camera import Camera
19
- from pygpt_net.controller.chat import Chat
20
- from pygpt_net.controller.command import Command
21
- from pygpt_net.controller.config import Config
22
- from pygpt_net.controller.ctx import Ctx
23
- from pygpt_net.controller.debug import Debug
24
- from pygpt_net.controller.dialogs import Dialogs
25
- from pygpt_net.controller.files import Files
26
- from pygpt_net.controller.finder import Finder
27
- from pygpt_net.controller.idx import Idx
28
- from pygpt_net.controller.kernel import Kernel
29
- from pygpt_net.controller.lang import Lang
30
- from pygpt_net.controller.launcher import Launcher
31
- from pygpt_net.controller.layout import Layout
32
- from pygpt_net.controller.media import Media
33
- from pygpt_net.controller.mode import Mode
34
- from pygpt_net.controller.model import Model
35
- from pygpt_net.controller.notepad import Notepad
36
- from pygpt_net.controller.painter import Painter
37
- from pygpt_net.controller.plugins import Plugins
38
- from pygpt_net.controller.realtime import Realtime
39
- from pygpt_net.controller.presets import Presets
40
- from pygpt_net.controller.settings import Settings
41
- from pygpt_net.controller.theme import Theme
42
- from pygpt_net.controller.tools import Tools
43
- from pygpt_net.controller.ui import UI
44
- from pygpt_net.utils import trans
12
+ from .access import Access
13
+ from .agent import Agent
14
+ from .assistant import Assistant
15
+ from .attachment import Attachment
16
+ from .audio import Audio
17
+ from .calendar import Calendar
18
+ from .camera import Camera
19
+ from .chat import Chat
20
+ from .command import Command
21
+ from .config import Config
22
+ from .ctx import Ctx
23
+ from .debug import Debug
24
+ from .dialogs import Dialogs
25
+ from .files import Files
26
+ from .finder import Finder
27
+ from .idx import Idx
28
+ from .kernel import Kernel
29
+ from .lang import Lang
30
+ from .launcher import Launcher
31
+ from .layout import Layout
32
+ from .media import Media
33
+ from .mode import Mode
34
+ from .model import Model
35
+ from .notepad import Notepad
36
+ from .painter import Painter
37
+ from .plugins import Plugins
38
+ from .realtime import Realtime
39
+ from .presets import Presets
40
+ from .settings import Settings
41
+ from .theme import Theme
42
+ from .tools import Tools
43
+ from .ui import UI
44
+
45
+ from pygpt_net.utils import trans, mem_clean
45
46
 
46
47
 
47
48
  class Controller:
@@ -154,36 +155,45 @@ class Controller:
154
155
 
155
156
  print(trans("status.reloading.profile.begin"))
156
157
 
157
- self.window.core.reload() # db, config, patch, etc.
158
- self.ui.tabs.reload()
159
- self.ctx.reload()
160
- self.settings.reload()
161
- self.assistant.reload()
162
- self.attachment.reload()
163
- self.presets.reload()
164
- self.idx.reload()
165
- self.agent.reload()
166
- self.calendar.reload()
167
- self.plugins.reload()
168
- self.painter.reload()
169
- self.notepad.reload()
170
- self.files.reload()
171
- self.lang.reload()
172
- self.debug.reload()
173
- self.chat.reload()
174
- self.media.reload()
175
- self.window.tools.on_reload()
176
- self.access.reload()
177
- self.tools.reload()
178
-
179
- # post-reload
180
- self.ui.tabs.reload_after()
181
- self.ctx.reload_after()
182
- self.ui.tabs.restore_data() # restore opened tabs data
183
- self.kernel.restart()
184
- self.theme.reload_all() # do not reload theme if no change
158
+ try:
159
+ self.window.core.reload() # db, config, patch, etc.
160
+ self.ui.tabs.reload()
161
+ self.ctx.reload()
162
+ self.settings.reload()
163
+ self.assistant.reload()
164
+ self.attachment.reload()
165
+ self.presets.reload()
166
+ self.idx.reload()
167
+ self.agent.reload()
168
+ self.calendar.reload()
169
+ self.plugins.reload()
170
+ self.painter.reload()
171
+ self.notepad.reload()
172
+ self.files.reload()
173
+ self.lang.reload()
174
+ self.debug.reload()
175
+ self.chat.reload()
176
+ self.media.reload()
177
+ self.window.tools.on_reload()
178
+ self.access.reload()
179
+ self.tools.reload()
180
+
181
+ # post-reload
182
+ self.ui.tabs.reload_after()
183
+ self.ctx.reload_after()
184
+ self.ui.tabs.restore_data() # restore opened tabs data
185
+ self.kernel.restart()
186
+ self.theme.reload_all() # do not reload theme if no change
187
+
188
+ except Exception as e:
189
+ self.window.core.debug.log(e)
185
190
 
186
191
  self.reloading = False # unlock
187
192
  self.presets.unlock()
188
193
 
194
+ try:
195
+ mem_clean(force=True) # try to clean memory
196
+ except Exception:
197
+ pass
198
+
189
199
  print(trans("status.reloading.profile.end"))
@@ -113,13 +113,6 @@ class Common:
113
113
  pinned_radio = nodes['filter.ctx.radio.pinned']
114
114
  indexed_radio = nodes['filter.ctx.radio.indexed']
115
115
 
116
- try:
117
- b1 = QSignalBlocker(all_radio)
118
- b2 = QSignalBlocker(pinned_radio)
119
- b3 = QSignalBlocker(indexed_radio)
120
- except:
121
- pass
122
-
123
116
  all_radio.setChecked(False)
124
117
  pinned_radio.setChecked(False)
125
118
  indexed_radio.setChecked(False)
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.24 23:00:00 #
9
+ # Updated Date: 2025.09.15 22:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Optional, List
@@ -47,6 +47,7 @@ class Ctx:
47
47
  # current group ID
48
48
  self.group_id = None
49
49
  self.selected = []
50
+ self._infinite_scroll_refresh = False
50
51
 
51
52
  def handle(self, event: BaseEvent):
52
53
  """
@@ -99,7 +100,10 @@ class Ctx:
99
100
 
100
101
  def setup(self):
101
102
  """Setup ctx"""
102
- self.common.restore_display_filter() # load filters first
103
+ try:
104
+ self.common.restore_display_filter() # load filters first
105
+ except Exception:
106
+ pass
103
107
 
104
108
  # load ctx list
105
109
  core = self.window.core
@@ -110,7 +114,9 @@ class Ctx:
110
114
  self.new()
111
115
  else:
112
116
  id = core.config.get('ctx')
113
- if id is not None and core.ctx.has(id):
117
+ if id is not None:
118
+ # Keep previously selected id; if it's not in the current page
119
+ # we will inject a placeholder into the list (see update_list).
114
120
  core.ctx.set_current(id)
115
121
  else:
116
122
  core.ctx.set_current(core.ctx.get_first())
@@ -395,13 +401,57 @@ class Ctx:
395
401
  :param reload: reload ctx list items
396
402
  :param restore_scroll: restore scroll position
397
403
  """
404
+ view = self.window.ui.nodes['ctx.list']
405
+
406
+ # Read raw scrollbar state BEFORE model rebuild
407
+ sb = None
408
+ prev_val = None
409
+ if restore_scroll:
410
+ try:
411
+ sb = view.verticalScrollBar()
412
+ prev_val = sb.value()
413
+ except Exception:
414
+ sb = None
415
+ prev_val = None
416
+
417
+ # In infinite-scroll mode do NOT call store/restore helpers to avoid collisions.
418
+ # Instead, schedule a pending scroll that will be applied while updates are disabled.
419
+ if self._infinite_scroll_refresh:
420
+ if prev_val is not None:
421
+ try:
422
+ view.set_pending_v_scroll(prev_val)
423
+ except Exception:
424
+ pass
425
+ else:
426
+ # Normal mode: keep existing helpers
427
+ try:
428
+ view.store_scroll_position()
429
+ except Exception:
430
+ pass
431
+
432
+ # Prepare data (with placeholder injection if current is outside of the page)
433
+ data = self.window.core.ctx.get_meta(reload)
434
+ data = self._inject_current_if_missing(data)
435
+
436
+ # Rebuild list (CtxList.update will apply pending scroll before enabling updates)
398
437
  self.window.ui.contexts.ctx_list.update(
399
438
  'ctx.list',
400
- self.window.core.ctx.get_meta(reload),
439
+ data,
401
440
  expand=False,
402
441
  )
442
+
443
+ # Post-fix: in normal mode let helpers restore; in infinite mode we already applied pending
403
444
  if restore_scroll:
404
- self.window.ui.nodes['ctx.list'].restore_scroll_position()
445
+ if self._infinite_scroll_refresh:
446
+ # Optional safety re-apply to the same value after layout settles
447
+ if sb is not None and prev_val is not None:
448
+ QTimer.singleShot(0, lambda: sb.setValue(prev_val))
449
+ else:
450
+ try:
451
+ view.restore_scroll_position()
452
+ QTimer.singleShot(0, view.restore_scroll_position)
453
+ except Exception:
454
+ pass
405
455
 
406
456
  def refresh(self, restore_model: bool = True):
407
457
  """
@@ -820,6 +870,7 @@ class Ctx:
820
870
 
821
871
  :param text: search string
822
872
  """
873
+ self.reset_loaded_total() # reset paging on new search
823
874
  self.window.core.ctx.clear_tmp_meta()
824
875
  self.window.core.ctx.set_search_string(text)
825
876
  self.window.core.config.set('ctx.search.string', text)
@@ -845,6 +896,7 @@ class Ctx:
845
896
 
846
897
  :param labels: list of labels
847
898
  """
899
+ self.reset_loaded_total() # reset paging on label filter change
848
900
  self.window.core.ctx.clear_tmp_meta()
849
901
  self.window.core.ctx.filters_labels = labels
850
902
  self.window.core.config.set('ctx.records.filter.labels', labels)
@@ -1235,6 +1287,7 @@ class Ctx:
1235
1287
 
1236
1288
  def reload(self):
1237
1289
  """Reload ctx"""
1290
+ self.reset_loaded_total() # reset paging
1238
1291
  self.window.core.ctx.reset()
1239
1292
  self.setup()
1240
1293
  self.update()
@@ -1305,4 +1358,117 @@ class Ctx:
1305
1358
  "meta": meta,
1306
1359
  }
1307
1360
  event = RenderEvent(RenderEvent.ON_LOAD, data)
1308
- self.window.dispatch(event)
1361
+ self.window.dispatch(event)
1362
+
1363
+ def get_package_limit(self) -> int:
1364
+ """Return base package size from config."""
1365
+ try:
1366
+ return int(self.window.core.config.get('ctx.records.limit') or 0)
1367
+ except Exception:
1368
+ return 0
1369
+
1370
+ def get_loaded_total_limit(self) -> int:
1371
+ """Return persisted total loaded size; fallback to base package size."""
1372
+ base = self.get_package_limit()
1373
+ try:
1374
+ val = int(self.window.core.config.get('ctx.records.limit.total') or 0)
1375
+ except Exception:
1376
+ val = 0
1377
+ return val if val > 0 else base
1378
+
1379
+ def reset_loaded_total(self):
1380
+ """Reset total loaded to base package; used on filter/search changes."""
1381
+ base = self.get_package_limit()
1382
+ # If base is 0 (unlimited), keep total as 0 which means "unlimited"
1383
+ self.window.core.config.set('ctx.records.limit.total', base if base > 0 else 0)
1384
+ self.window.core.config.save()
1385
+
1386
+ def load_more(self, packages: int = 1):
1387
+ """
1388
+ Increase total loaded limit and append new ungrouped records without rebuilding the list.
1389
+ """
1390
+ base = self.get_package_limit()
1391
+ if base <= 0:
1392
+ # Unlimited mode – nothing to do
1393
+ return
1394
+
1395
+ current_total = self.get_loaded_total_limit()
1396
+ new_total = current_total + max(1, int(packages)) * base
1397
+ self.window.core.config.set('ctx.records.limit.total', new_total)
1398
+ self.window.core.config.save()
1399
+
1400
+ # If folders are at top (default), we can safely append without rebuilding.
1401
+ # Otherwise, fall back to safe rebuild with stable scroll restore.
1402
+ folders_top = bool(self.window.core.config.get("ctx.records.folders.top"))
1403
+
1404
+ # Pull fresh meta with increased total
1405
+ data = self.window.core.ctx.get_meta(reload=True)
1406
+
1407
+ if folders_top:
1408
+ # compute which ungrouped & not pinned IDs are already visible
1409
+ view = self.window.ui.nodes['ctx.list']
1410
+ visible_ids = set()
1411
+ try:
1412
+ visible_ids = view.get_visible_unpaged_ids()
1413
+ except Exception:
1414
+ pass
1415
+
1416
+ # build candidate list in provider order
1417
+ candidates = []
1418
+ for mid, meta in data.items():
1419
+ if (meta.group_id is None or meta.group_id == 0) and not meta.important:
1420
+ candidates.append(mid)
1421
+
1422
+ # filter only new ones (keep order)
1423
+ add_ids = [mid for mid in candidates if mid not in visible_ids]
1424
+
1425
+ # append without rebuilding
1426
+ self.window.ui.contexts.ctx_list.append_unpaginated('ctx.list', data, add_ids)
1427
+ else:
1428
+ # Fallback: rare config where groups are placed after items – do a stable rebuild
1429
+ view = self.window.ui.nodes['ctx.list']
1430
+ try:
1431
+ sb = view.verticalScrollBar()
1432
+ prev_val = sb.value()
1433
+ # mark special refresh to avoid collisions with helpers
1434
+ self._infinite_scroll_refresh = True
1435
+ # set pending to preserve old position during rebuild
1436
+ view.set_pending_v_scroll(prev_val)
1437
+ except Exception:
1438
+ prev_val = None
1439
+
1440
+ try:
1441
+ self.update_list(reload=True, restore_scroll=True)
1442
+ finally:
1443
+ self._infinite_scroll_refresh = False
1444
+
1445
+ def _inject_current_if_missing(self, data: dict) -> dict:
1446
+ """
1447
+ Inject currently selected meta at the end of the list if it's not in the current page.
1448
+ This produces a 'placeholder' entry that will be replaced by the real one once loaded.
1449
+ """
1450
+ try:
1451
+ cur_id = self.window.core.ctx.get_current()
1452
+ if cur_id is None:
1453
+ cur_id = self.window.core.config.get('ctx')
1454
+ if not cur_id or cur_id in data:
1455
+ return data
1456
+
1457
+ provider = self.window.core.ctx.get_provider()
1458
+ meta = provider.get_meta_by_id(cur_id)
1459
+ if meta is None:
1460
+ return data # nothing to inject
1461
+
1462
+ # Mark as placeholder in 'extra' for potential future diagnostics; UI ignores this flag.
1463
+ try:
1464
+ if meta.extra is None:
1465
+ meta.extra = {}
1466
+ meta.extra['__placeholder'] = True
1467
+ except Exception:
1468
+ pass
1469
+
1470
+ injected = dict(data) # keep original ordering, append at the end
1471
+ injected[meta.id] = meta
1472
+ return injected
1473
+ except Exception:
1474
+ return data
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.07 22:00:00 #
9
+ # Updated Date: 2025.09.15 22:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtWidgets import QApplication
@@ -67,8 +67,8 @@ class Extra:
67
67
  :param value: block text
68
68
  """
69
69
  QApplication.clipboard().setText(value.strip())
70
- suffix = value[:20] + "..." if len(value) > 20 else value
71
- self.window.update_status(trans("clipboard.copied_to") + " " + suffix)
70
+ suffix = f"{value[:20]}..." if len(value) > 20 else value
71
+ self.window.update_status(f"{trans('clipboard.copied_to')} {suffix}")
72
72
 
73
73
  def preview_code_text(self, value: str):
74
74
  """
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.24 02:00:00 #
9
+ # Updated Date: 2025.09.15 22:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
@@ -163,6 +163,7 @@ class Editor:
163
163
  self.config_changed('ctx.records.groups.separators') or
164
164
  self.config_changed('ctx.records.pinned.separators') or
165
165
  self.config_changed('ctx.records.separators')):
166
+ self.window.controller.ctx.reset_loaded_total() # reset paging
166
167
  self.window.controller.ctx.update()
167
168
 
168
169
  # syntax highlighter style
@@ -320,6 +321,7 @@ class Editor:
320
321
  # update ctx limit
321
322
  elif key.startswith('ctx.records.limit') and caller == "slider":
322
323
  self.window.core.config.set(key, value)
324
+ self.window.controller.ctx.reset_loaded_total() # reset paging
323
325
  QTimer.singleShot(1000, lambda: self.window.controller.ctx.update(True, False))
324
326
 
325
327
  # update layout density
@@ -81,11 +81,17 @@ class Common:
81
81
  if state:
82
82
  for node in nodes:
83
83
  if node in self.window.ui.nodes:
84
- self.window.ui.nodes[node].setVisible(True)
84
+ try:
85
+ self.window.ui.nodes[node].setVisible(True)
86
+ except Exception:
87
+ pass
85
88
  else:
86
89
  for node in nodes:
87
90
  if node in self.window.ui.nodes:
88
- self.window.ui.nodes[node].setVisible(False)
91
+ try:
92
+ self.window.ui.nodes[node].setVisible(False)
93
+ except Exception:
94
+ pass
89
95
 
90
96
  self.window.ui.menu['theme.tooltips'].setChecked(state)
91
97
 
@@ -134,46 +134,15 @@ class Tabs:
134
134
 
135
135
  def reload(self):
136
136
  """Reload tabs"""
137
- self.unload_current()
137
+ columns = self.window.ui.layout.columns
138
+ for col in columns:
139
+ col.setUpdatesEnabled(False)
140
+ self.window.core.tabs.remove_all()
138
141
  self.window.core.tabs.reload()
139
142
  self.window.dispatch(RenderEvent(RenderEvent.PREPARE))
140
143
  self.debug()
141
-
142
- def unload_current(self):
143
- """Unload current tabs from memory"""
144
- tabs = self.window.core.tabs.pids
145
- for pid in tabs:
146
- tab = self.window.core.tabs.get_tab_by_pid(pid)
147
- if tab is not None:
148
- try:
149
- if tab.type == Tab.TAB_CHAT:
150
- node = self.window.ui.nodes['output'].get(tab.pid)
151
- if node:
152
- node.hide()
153
- p = node.page()
154
- p.triggerAction(QWebEnginePage.Stop)
155
- p.setUrl(QUrl("about:blank"))
156
- p.history().clear()
157
- p.setLifecycleState(QWebEnginePage.LifecycleState.Discarded)
158
- tab.delete_ref(node)
159
- layout = tab.child.layout()
160
- layout.removeWidget(node)
161
- self.window.ui.nodes['output'].pop(pid, None)
162
- node.on_delete()
163
- node_plain = self.window.ui.nodes['output_plain'].get(tab.pid)
164
- if node_plain:
165
- tab.delete_ref(node_plain)
166
- layout = tab.child.layout()
167
- layout.removeWidget(node_plain)
168
- self.window.ui.nodes['output_plain'].pop(pid, None)
169
- node_plain.on_delete()
170
- tab.cleanup()
171
- except Exception as e:
172
- print(f"Error unloading tab {pid}: {e}")
173
- try:
174
- gc.collect()
175
- except Exception:
176
- pass
144
+ for col in columns:
145
+ col.setUpdatesEnabled(True)
177
146
 
178
147
  def reload_after(self):
179
148
  """Reload tabs after"""