pygpt-net 2.6.51__py3-none-any.whl → 2.6.53__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.
Files changed (53) hide show
  1. pygpt_net/CHANGELOG.txt +10 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +3 -1
  4. pygpt_net/controller/__init__.py +4 -2
  5. pygpt_net/controller/audio/audio.py +22 -1
  6. pygpt_net/controller/chat/chat.py +5 -1
  7. pygpt_net/controller/chat/remote_tools.py +116 -0
  8. pygpt_net/controller/ctx/ctx.py +8 -3
  9. pygpt_net/controller/lang/mapping.py +2 -1
  10. pygpt_net/controller/mode/mode.py +5 -2
  11. pygpt_net/controller/plugins/plugins.py +29 -3
  12. pygpt_net/controller/realtime/realtime.py +8 -3
  13. pygpt_net/controller/ui/mode.py +11 -5
  14. pygpt_net/controller/ui/tabs.py +31 -7
  15. pygpt_net/core/ctx/output.py +4 -2
  16. pygpt_net/core/render/web/renderer.py +5 -4
  17. pygpt_net/core/tabs/tab.py +42 -9
  18. pygpt_net/core/tabs/tabs.py +7 -9
  19. pygpt_net/data/config/config.json +6 -5
  20. pygpt_net/data/config/models.json +3 -3
  21. pygpt_net/data/icons/web_off.svg +1 -0
  22. pygpt_net/data/icons/web_on.svg +1 -0
  23. pygpt_net/data/locale/locale.de.ini +1 -0
  24. pygpt_net/data/locale/locale.en.ini +3 -2
  25. pygpt_net/data/locale/locale.es.ini +1 -0
  26. pygpt_net/data/locale/locale.fr.ini +1 -0
  27. pygpt_net/data/locale/locale.it.ini +1 -0
  28. pygpt_net/data/locale/locale.pl.ini +1 -4
  29. pygpt_net/data/locale/locale.uk.ini +1 -0
  30. pygpt_net/data/locale/locale.zh.ini +1 -0
  31. pygpt_net/data/locale/plugin.mcp.en.ini +12 -0
  32. pygpt_net/icons.qrc +2 -0
  33. pygpt_net/icons_rc.py +232 -147
  34. pygpt_net/plugin/mcp/__init__.py +12 -0
  35. pygpt_net/plugin/mcp/config.py +103 -0
  36. pygpt_net/plugin/mcp/plugin.py +513 -0
  37. pygpt_net/plugin/mcp/worker.py +263 -0
  38. pygpt_net/provider/api/anthropic/tools.py +4 -2
  39. pygpt_net/provider/api/google/__init__.py +3 -2
  40. pygpt_net/provider/api/openai/agents/remote_tools.py +14 -4
  41. pygpt_net/provider/api/openai/chat.py +14 -2
  42. pygpt_net/provider/api/openai/remote_tools.py +5 -2
  43. pygpt_net/provider/api/x_ai/remote.py +6 -1
  44. pygpt_net/provider/core/config/patch.py +8 -1
  45. pygpt_net/ui/dialog/plugins.py +1 -3
  46. pygpt_net/ui/layout/chat/output.py +7 -2
  47. pygpt_net/ui/widget/element/labels.py +1 -2
  48. pygpt_net/ui/widget/tabs/body.py +24 -5
  49. {pygpt_net-2.6.51.dist-info → pygpt_net-2.6.53.dist-info}/METADATA +24 -4
  50. {pygpt_net-2.6.51.dist-info → pygpt_net-2.6.53.dist-info}/RECORD +53 -45
  51. {pygpt_net-2.6.51.dist-info → pygpt_net-2.6.53.dist-info}/LICENSE +0 -0
  52. {pygpt_net-2.6.51.dist-info → pygpt_net-2.6.53.dist-info}/WHEEL +0 -0
  53. {pygpt_net-2.6.51.dist-info → pygpt_net-2.6.53.dist-info}/entry_points.txt +0 -0
pygpt_net/CHANGELOG.txt CHANGED
@@ -1,3 +1,13 @@
1
+ 2.6.53 (2025-09-17)
2
+
3
+ - Added: An icon to enable/disable the web search remote tool in the icon bar, along with remote web search functionality in OpenRouter (#135).
4
+ - Added: The ability to mute audio in real-time mode via the audio icon.
5
+
6
+ 2.6.52 (2025-09-17)
7
+
8
+ - Added MCP plugin: Provides access to remote tools via the Model Context Protocol (MCP), including stdio, SSE, and Streamable HTTP transports, with per-server allow/deny filtering, Authorization header support, and a tools cache.
9
+ - Fixed: tab tooltips reload on profile switch.
10
+
1
11
  2.6.51 (2025-09-16)
2
12
 
3
13
  - Fix: Automatically reloading calendar notes.
pygpt_net/__init__.py CHANGED
@@ -6,15 +6,15 @@
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.16 00:00:00 #
9
+ # Updated Date: 2025.09.17 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  __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.51"
17
- __build__ = "2025-09-16"
16
+ __version__ = "2.6.53"
17
+ __build__ = "2025-09-17"
18
18
  __maintainer__ = "Marcin Szczygliński"
19
19
  __github__ = "https://github.com/szczyglis-dev/py-gpt"
20
20
  __report__ = "https://github.com/szczyglis-dev/py-gpt/issues"
pygpt_net/app.py CHANGED
@@ -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.09.11 08:00:00 #
9
+ # Updated Date: 2025.09.16 22:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -96,6 +96,7 @@ from pygpt_net.plugin.bitbucket import Plugin as BitbucketPlugin
96
96
  from pygpt_net.plugin.server import Plugin as ServerPlugin
97
97
  from pygpt_net.plugin.tuya import Plugin as TuyaPlugin
98
98
  from pygpt_net.plugin.wikipedia import Plugin as WikipediaPlugin
99
+ from pygpt_net.plugin.mcp import Plugin as MCPPlugin
99
100
 
100
101
  # agents (Llama-index)
101
102
  # from pygpt_net.provider.agents.llama_index.legacy.openai import OpenAIAgent
@@ -425,6 +426,7 @@ def run(**kwargs):
425
426
  launcher.add_plugin(ServerPlugin())
426
427
  launcher.add_plugin(TuyaPlugin())
427
428
  launcher.add_plugin(WikipediaPlugin())
429
+ launcher.add_plugin(MCPPlugin())
428
430
 
429
431
  # register custom plugins
430
432
  plugins = kwargs.get('plugins', None)
@@ -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.09.15 23:00:00 #
9
+ # Updated Date: 2025.09.16 22:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from .access import Access
@@ -156,9 +156,12 @@ class Controller:
156
156
  print(trans("status.reloading.profile.begin"))
157
157
 
158
158
  try:
159
+ self.ui.tabs.locked = True # lock tabs
159
160
  self.window.core.reload() # db, config, patch, etc.
160
161
  self.ui.tabs.reload()
161
162
  self.ctx.reload()
163
+ self.ui.tabs.locked = False # unlock tabs
164
+
162
165
  self.settings.reload()
163
166
  self.assistant.reload()
164
167
  self.attachment.reload()
@@ -181,7 +184,6 @@ class Controller:
181
184
  # post-reload
182
185
  self.ui.tabs.reload_after()
183
186
  self.ctx.reload_after()
184
- self.ui.tabs.restore_data() # restore opened tabs data
185
187
  self.kernel.restart()
186
188
  self.theme.reload_all() # do not reload theme if no change
187
189
 
@@ -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.31 23:00:00 #
9
+ # Updated Date: 2025.09.17 07:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -36,6 +36,7 @@ class Audio:
36
36
  Tab.TAB_CHAT,
37
37
  Tab.TAB_TOOL_CALENDAR,
38
38
  ]
39
+ self.muted = False
39
40
 
40
41
  def setup(self):
41
42
  """Setup controller"""
@@ -53,6 +54,26 @@ class Audio:
53
54
  if self.window.core.config.get("audio.input.loop", False):
54
55
  self.window.ui.nodes['audio.loop'].box.setChecked(True)
55
56
 
57
+ def is_muted(self) -> bool:
58
+ """
59
+ Check if audio output is muted
60
+
61
+ :return: True if muted
62
+ """
63
+ return self.muted
64
+
65
+ def set_muted(self, state: bool):
66
+ """
67
+ Set audio output muted state
68
+
69
+ :param state: True to mute, False to unmute
70
+ """
71
+ self.muted = state
72
+ if state:
73
+ self.toggle_output_icon(False)
74
+ else:
75
+ self.toggle_output_icon(True)
76
+
56
77
  def execute_input_stop(self):
57
78
  """Execute input stop (from UI)"""
58
79
  self.window.dispatch(Event(Event.AUDIO_INPUT_RECORD_TOGGLE, {
@@ -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.23 15:00:00 #
9
+ # Updated Date: 2025.09.17 05:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Any
@@ -22,6 +22,7 @@ from .files import Files
22
22
  from .image import Image
23
23
  from .input import Input
24
24
  from .output import Output
25
+ from .remote_tools import RemoteTools
25
26
  from .render import Render
26
27
  from .response import Response
27
28
  from .stream import Stream
@@ -46,6 +47,7 @@ class Chat:
46
47
  self.image = Image(w)
47
48
  self.input = Input(w)
48
49
  self.output = Output(w)
50
+ self.remote_tools = RemoteTools(w)
49
51
  self.render = Render(w)
50
52
  self.response = Response(w)
51
53
  self.stream = Stream(w)
@@ -60,12 +62,14 @@ class Chat:
60
62
  """Setup"""
61
63
  self.common.setup()
62
64
  self.attachment.setup()
65
+ self.remote_tools.setup()
63
66
 
64
67
  def reload(self) -> None:
65
68
  """Reload"""
66
69
  self.common.setup()
67
70
  self.render.reload()
68
71
  self.attachment.reload()
72
+ self.remote_tools.setup()
69
73
 
70
74
  def handle_error(self, err: Any) -> None:
71
75
  """
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2025.09.17 05:00:00 #
10
+ # ================================================== #
11
+
12
+ from typing import Union
13
+
14
+ from pygpt_net.item.model import ModelItem
15
+
16
+
17
+ class RemoteTools:
18
+ def __init__(self, window=None):
19
+ """
20
+ Remote tools controller
21
+
22
+ :param window: Window instance
23
+ """
24
+ self.window = window
25
+ self.enabled_global = {
26
+ "web_search": False,
27
+ }
28
+
29
+ def setup(self):
30
+ """
31
+ Setup remote tools
32
+
33
+ :return: None
34
+ """
35
+ cfg_get = self.window.core.config.get
36
+ self.enabled_global["web_search"] = cfg_get("remote_tools.global.web_search", False)
37
+ self.update_icons()
38
+
39
+ def enabled(self, model: Union[ModelItem, str], tool_name: str) -> bool:
40
+ """
41
+ Check if remote tool is enabled
42
+
43
+ :param model: ModelItem or model name
44
+ :param tool_name: Tool name
45
+ :return: True if enabled, False otherwise
46
+ """
47
+ if isinstance(model, str):
48
+ model = self.window.core.models.get(model)
49
+ if not model:
50
+ return False
51
+ if tool_name == "web_search":
52
+ return self.is_web(model)
53
+ return False
54
+
55
+ def is_web(self, model: ModelItem) -> bool:
56
+ """
57
+ Check if web search is enabled for the given provider
58
+
59
+ :param model: ModelItem
60
+ :return: True if web search is enabled, False otherwise
61
+ """
62
+ # at first, check provider-specific config
63
+ cfg_get = self.window.core.config.get
64
+ state = False
65
+ if model.provider == "openai": # native SDK, responses API
66
+ state = cfg_get("remote_tools.web_search", False)
67
+ elif model.provider == "google": # native SDK
68
+ state = cfg_get("remote_tools.google.web_search", False)
69
+ elif model.provider == "anthropic": # native SDK
70
+ state = cfg_get("remote_tools.anthropic.web_search", False)
71
+ elif model.provider == "x_ai": # native SDK
72
+ mode = cfg_get("remote_tools.xai.mode", "auto")
73
+ if mode not in ("auto", "on", "off"):
74
+ mode = "auto"
75
+ if mode == "auto" or mode == "on":
76
+ state = True
77
+
78
+ # if not enabled by default or other provider, then use global config
79
+ if not state:
80
+ state = self.enabled_global["web_search"]
81
+
82
+ return state
83
+
84
+ def update_icons(self):
85
+ """
86
+ Update remote tools icons in chat tabs
87
+ """
88
+ state = self.enabled_global["web_search"]
89
+ if state:
90
+ self.window.ui.nodes['icon.remote_tool.web'].set_icon(":/icons/web_on.svg")
91
+ else:
92
+ self.window.ui.nodes['icon.remote_tool.web'].set_icon(":/icons/web_off.svg")
93
+
94
+ def toggle(self, tool_name: str):
95
+ """
96
+ Toggle remote tool (for global toggle button)
97
+
98
+ :param tool_name: Tool name
99
+ """
100
+ cfg_set = self.window.core.config.set
101
+
102
+ # tool: web search
103
+ if tool_name == "web_search":
104
+ state = not self.enabled_global["web_search"]
105
+ self.enabled_global["web_search"] = state
106
+ cfg_set("remote_tools.global.web_search", state)
107
+ cfg_set("remote_tools.web_search", state)
108
+ cfg_set("remote_tools.google.web_search", state)
109
+ cfg_set("remote_tools.anthropic.web_search", state)
110
+
111
+ # xAI has 3 modes: auto, on, off
112
+ cfg_set("remote_tools.xai.mode", "auto" if state else "off")
113
+
114
+ # save config
115
+ self.window.core.config.save()
116
+ self.update_icons()
@@ -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.09.16 02:00:00 #
9
+ # Updated Date: 2025.09.16 22:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Optional, List
@@ -1289,14 +1289,19 @@ class Ctx:
1289
1289
 
1290
1290
  def reload(self):
1291
1291
  """Reload ctx"""
1292
+ self.edit_meta_id = None
1293
+ self.edit_item_id = None
1294
+ self.group_id = None
1295
+ self.selected = []
1296
+ self._infinite_scroll_refresh = False
1292
1297
  self.reset_loaded_total() # reset paging
1293
1298
  self.window.core.ctx.reset()
1294
1299
  self.setup()
1295
- self.update()
1296
- self.refresh()
1297
1300
 
1298
1301
  def reload_after(self):
1299
1302
  """After reload"""
1303
+ self.update()
1304
+ self.refresh()
1300
1305
  self.new_if_empty()
1301
1306
 
1302
1307
  def add_selected(self, id: int):
@@ -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.15 23:00:00 #
9
+ # Updated Date: 2025.09.17 05:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Dict
@@ -423,6 +423,7 @@ class Mapping:
423
423
  tooltips['icon.video.capture'] = 'icon.video.capture'
424
424
  tooltips['icon.audio.output'] = 'icon.audio.output'
425
425
  tooltips['icon.audio.input'] = 'icon.audio.input'
426
+ tooltips['icon.remote_tool.web'] = 'icon.remote_tool.web'
426
427
  tooltips['assistant.store.btn.refresh_status'] = 'dialog.assistant.store.btn.refresh_status'
427
428
  tooltips['agent.llama.loop.score'] = 'toolbox.agent.llama.loop.score.tooltip'
428
429
 
@@ -6,13 +6,13 @@
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.28 09:00:00 #
9
+ # Updated Date: 2025.09.17 07:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from pygpt_net.core.events import Event, AppEvent
13
13
  from pygpt_net.core.types import (
14
14
  MODE_ASSISTANT,
15
- MODE_CHAT,
15
+ MODE_CHAT, MODE_AUDIO,
16
16
  )
17
17
  from pygpt_net.item.ctx import CtxItem
18
18
  from pygpt_net.utils import trans
@@ -92,6 +92,9 @@ class Mode:
92
92
  else:
93
93
  c.assistant.select_current()
94
94
 
95
+ elif mode == MODE_AUDIO:
96
+ c.audio.set_muted(False) # un-mute and show audio output icon by default
97
+
95
98
  cfg.set('mode', mode)
96
99
 
97
100
  # reset model and preset at start
@@ -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.09.05 18:00:00 #
9
+ # Updated Date: 2025.09.17 07:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import List, Dict, Any, Optional
@@ -14,7 +14,7 @@ from typing import List, Dict, Any, Optional
14
14
  from PySide6.QtGui import QAction
15
15
 
16
16
  from pygpt_net.core.types import (
17
- MODE_AGENT,
17
+ MODE_AGENT, MODE_AUDIO,
18
18
  )
19
19
  from pygpt_net.controller.plugins.presets import Presets
20
20
  from pygpt_net.controller.plugins.settings import Settings
@@ -194,20 +194,46 @@ class Plugins:
194
194
 
195
195
  def toggle(self, id: str):
196
196
  """
197
- Toggle plugin
197
+ Toggle plugin (from menu)
198
198
 
199
199
  :param id: plugin id
200
200
  """
201
201
  if self.window.core.plugins.is_registered(id):
202
202
  if self.is_enabled(id):
203
203
  self.disable(id)
204
+ if id == "audio_output":
205
+ self.window.controller.audio.set_muted(True)
204
206
  else:
205
207
  self.enable(id)
208
+ if id == "audio_output":
209
+ self.window.controller.audio.set_muted(False)
206
210
 
207
211
  self.window.controller.ui.update_tokens()
208
212
  self.window.controller.attachment.update()
209
213
  self.presets.save_current()
210
214
 
215
+ def toggle_audio_output(self):
216
+ """
217
+ Toggle plugin (from icon, audio output only)
218
+ """
219
+ id = "audio_output"
220
+ mode = self.window.core.config.get('mode')
221
+
222
+ if mode == MODE_AUDIO:
223
+ if not self.window.controller.audio.is_muted():
224
+ self.window.controller.audio.set_muted(True)
225
+ else:
226
+ self.window.controller.audio.set_muted(False)
227
+ return
228
+
229
+ if self.window.core.plugins.is_registered(id):
230
+ if self.is_enabled(id):
231
+ self.disable(id)
232
+ else:
233
+ self.enable(id)
234
+
235
+ self.presets.save_current()
236
+
211
237
  def set_by_tab(self, idx: int):
212
238
  """
213
239
  Set current plugin by tab index
@@ -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.31 23:00:00 #
9
+ # Updated Date: 2025.09.17 07:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import Slot, QTimer
@@ -64,6 +64,8 @@ class Realtime:
64
64
 
65
65
  :param event: RealtimeEvent instance
66
66
  """
67
+ is_muted = self.window.controller.audio.is_muted() # global mute state
68
+
67
69
  # check if mode is supported
68
70
  if not self.is_supported() and isinstance(event, RealtimeEvent):
69
71
  event.stop = True # stop further propagation
@@ -75,7 +77,7 @@ class Realtime:
75
77
  if event.name == RealtimeEvent.RT_OUTPUT_AUDIO_DELTA:
76
78
  self.set_idle()
77
79
  payload = event.data.get("payload", None)
78
- if payload:
80
+ if payload and not is_muted: # do not play if muted
79
81
  self.window.core.audio.output.handle_realtime(payload, self.signals)
80
82
 
81
83
  # audio input chunk: send to the active realtime client
@@ -148,7 +150,10 @@ class Realtime:
148
150
 
149
151
  # volume change: update volume in audio output handler
150
152
  elif event.name == RealtimeEvent.RT_OUTPUT_AUDIO_VOLUME_CHANGED:
151
- volume = event.data.get("volume", 1.0)
153
+ if not is_muted:
154
+ volume = event.data.get("volume", 1.0)
155
+ else:
156
+ volume = 0.0
152
157
  self.window.controller.audio.ui.on_output_volume_change(volume)
153
158
 
154
159
  # error: audio output error
@@ -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.09.01 23:00:00 #
9
+ # Updated Date: 2025.09.17 07:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from pygpt_net.core.types import (
@@ -59,11 +59,17 @@ class Mode:
59
59
  is_audio = mode == MODE_AUDIO
60
60
 
61
61
  if not is_audio:
62
- self.window.ui.nodes['audio.auto_turn'].setVisible(False)
63
- self.window.ui.nodes["audio.loop"].setVisible(False)
62
+ ui_nodes['audio.auto_turn'].setVisible(False)
63
+ ui_nodes["audio.loop"].setVisible(False)
64
+ if not ctrl.plugins.is_enabled('audio_output'):
65
+ ctrl.audio.toggle_output_icon(False)
64
66
  else:
65
- self.window.ui.nodes['audio.auto_turn'].setVisible(True)
66
- self.window.ui.nodes["audio.loop"].setVisible(True)
67
+ ui_nodes['audio.auto_turn'].setVisible(True)
68
+ ui_nodes["audio.loop"].setVisible(True)
69
+ if not ctrl.audio.is_muted():
70
+ ctrl.audio.toggle_output_icon(True)
71
+ else:
72
+ ctrl.audio.toggle_output_icon(False)
67
73
 
68
74
  if not is_assistant:
69
75
  ui_nodes['presets.widget'].setVisible(True)
@@ -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.09.16 02:00:00 #
9
+ # Updated Date: 2025.09.16 22:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Any, Optional, Tuple
@@ -40,12 +40,13 @@ class Tabs:
40
40
  self.create_new_on_tab = True
41
41
  self.col = {}
42
42
 
43
- def setup(self):
43
+ def setup(self, reload: bool = False):
44
44
  """Setup tabs"""
45
45
  w = self.window
46
46
  w.core.tabs.load()
47
47
  w.controller.notepad.load()
48
- self.setup_options()
48
+ if not reload:
49
+ self.setup_options()
49
50
  self.initialized = True
50
51
 
51
52
  def setup_options(self):
@@ -130,13 +131,31 @@ class Tabs:
130
131
  self.col[curr_column] = curr_tab.pid
131
132
  self.debug()
132
133
 
134
+ def unload(self):
135
+ """Unload tabs"""
136
+ self.active_idx = 0
137
+ self.prev_idx = 0
138
+ self.appended = False
139
+ self.current = 0
140
+ self.column_idx = 0
141
+ self.tmp_column_idx = 0
142
+ self.create_new_on_tab = True
143
+ self.col = {}
144
+ columns = self.window.ui.layout.columns
145
+ for col in columns:
146
+ col.setUpdatesEnabled(False)
147
+ self.window.core.tabs.remove_all()
148
+ for col in columns:
149
+ col.setUpdatesEnabled(True)
150
+
133
151
  def reload(self):
134
152
  """Reload tabs"""
153
+ self.unload()
135
154
  columns = self.window.ui.layout.columns
136
155
  for col in columns:
137
156
  col.setUpdatesEnabled(False)
138
- self.window.core.tabs.remove_all()
139
- self.window.core.tabs.reload()
157
+ self.setup(reload=True)
158
+ self.restore_data()
140
159
  self.window.dispatch(RenderEvent(RenderEvent.PREPARE))
141
160
  self.debug()
142
161
  for col in columns:
@@ -172,6 +191,9 @@ class Tabs:
172
191
  :param idx: tab index
173
192
  :param column_idx: column index
174
193
  """
194
+ if idx == -1:
195
+ return
196
+
175
197
  w = self.window
176
198
  core = w.core
177
199
  tabs_core = core.tabs
@@ -594,7 +616,9 @@ class Tabs:
594
616
 
595
617
  :param tooltip: tooltip text
596
618
  """
597
- tabs = self.window.ui.layout.get_active_tabs()
619
+ tabs = self.window.ui.layout.get_tabs_by_idx(self.column_idx)
620
+ if tabs is not None and 0 <= self.current < tabs.count():
621
+ tabs.setTabToolTip(self.current, tooltip)
598
622
  tabs.setTabToolTip(self.current, tooltip)
599
623
  self.debug()
600
624
 
@@ -656,7 +680,7 @@ class Tabs:
656
680
  """
657
681
  if self.get_current_type() != Tab.TAB_CHAT:
658
682
  return
659
- tabs = self.window.ui.layout.get_active_tabs()
683
+ tabs = self.window.ui.layout.get_tabs_by_idx(self.column_idx)
660
684
  tooltip = title
661
685
  tabs.setTabToolTip(idx, tooltip)
662
686
  if len(title) > self.TAB_CHAT_MAX_CHARS:
@@ -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.16 22:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Optional, List, Dict
@@ -71,7 +71,7 @@ class Output:
71
71
  col_idx = tab.column_idx
72
72
 
73
73
  col_map = self.mapping.get(col_idx)
74
- if col_map is None:
74
+ if col_map is None or not isinstance(col_map, dict):
75
75
  col_map = self.mapping[col_idx] = {}
76
76
  last_map = self.last_pids.get(col_idx)
77
77
  if last_map is None:
@@ -175,6 +175,8 @@ class Output:
175
175
  return None
176
176
  col_idx = tab.column_idx
177
177
  col_map = self.mapping.get(col_idx, {})
178
+ if not isinstance(col_map, dict):
179
+ return None
178
180
 
179
181
  candidates = [pid for pid, meta_id in col_map.items() if meta_id == meta.id]
180
182
  for pid in candidates:
@@ -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.09.16 02:00:00 #
9
+ # Updated Date: 2025.09.17 05:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import gc
@@ -1289,12 +1289,12 @@ class Renderer(BaseRenderer):
1289
1289
  :param meta: context meta
1290
1290
  """
1291
1291
  tab = node.get_tab()
1292
+ if tab is None:
1293
+ return
1292
1294
  layout = tab.child.layout()
1293
- tab.child.remove_widget(node)
1295
+ tab.unwrap(node)
1294
1296
  self.window.ui.nodes['output'].pop(tab.pid, None)
1295
1297
 
1296
- node.on_delete()
1297
-
1298
1298
  view = ChatWebOutput(self.window)
1299
1299
  view.set_tab(tab)
1300
1300
  view.set_meta(meta)
@@ -1302,6 +1302,7 @@ class Renderer(BaseRenderer):
1302
1302
  view.signals.audio_read.connect(self.window.controller.chat.render.handle_audio_read)
1303
1303
 
1304
1304
  layout.addWidget(view) # tab body layout
1305
+ tab.add_ref(view)
1305
1306
  view.setVisible(True)
1306
1307
  self.window.ui.nodes['output'][tab.pid] = view
1307
1308
  try: