pygpt-net 2.7.5__py3-none-any.whl → 2.7.7__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 (82) hide show
  1. pygpt_net/CHANGELOG.txt +14 -0
  2. pygpt_net/__init__.py +4 -4
  3. pygpt_net/controller/chat/remote_tools.py +3 -9
  4. pygpt_net/controller/chat/stream.py +2 -2
  5. pygpt_net/controller/chat/{handler/worker.py → stream_worker.py} +20 -64
  6. pygpt_net/controller/debug/fixtures.py +3 -2
  7. pygpt_net/controller/files/files.py +65 -4
  8. pygpt_net/core/debug/models.py +2 -2
  9. pygpt_net/core/filesystem/url.py +4 -1
  10. pygpt_net/core/render/web/body.py +3 -2
  11. pygpt_net/core/types/chunk.py +27 -0
  12. pygpt_net/data/config/config.json +14 -4
  13. pygpt_net/data/config/models.json +192 -4
  14. pygpt_net/data/config/settings.json +126 -36
  15. pygpt_net/data/js/app/template.js +1 -1
  16. pygpt_net/data/js/app.min.js +2 -2
  17. pygpt_net/data/locale/locale.de.ini +5 -0
  18. pygpt_net/data/locale/locale.en.ini +35 -8
  19. pygpt_net/data/locale/locale.es.ini +5 -0
  20. pygpt_net/data/locale/locale.fr.ini +5 -0
  21. pygpt_net/data/locale/locale.it.ini +5 -0
  22. pygpt_net/data/locale/locale.pl.ini +5 -0
  23. pygpt_net/data/locale/locale.uk.ini +5 -0
  24. pygpt_net/data/locale/locale.zh.ini +5 -0
  25. pygpt_net/data/locale/plugin.cmd_mouse_control.en.ini +2 -2
  26. pygpt_net/item/ctx.py +3 -5
  27. pygpt_net/js_rc.py +2449 -2447
  28. pygpt_net/plugin/cmd_mouse_control/config.py +8 -7
  29. pygpt_net/plugin/cmd_mouse_control/plugin.py +3 -4
  30. pygpt_net/plugin/cmd_mouse_control/worker.py +2 -1
  31. pygpt_net/plugin/cmd_mouse_control/worker_sandbox.py +2 -1
  32. pygpt_net/provider/api/anthropic/__init__.py +16 -9
  33. pygpt_net/provider/api/anthropic/chat.py +259 -11
  34. pygpt_net/provider/api/anthropic/computer.py +844 -0
  35. pygpt_net/provider/api/anthropic/remote_tools.py +172 -0
  36. pygpt_net/{controller/chat/handler/anthropic_stream.py → provider/api/anthropic/stream.py} +24 -10
  37. pygpt_net/provider/api/anthropic/tools.py +32 -77
  38. pygpt_net/provider/api/anthropic/utils.py +30 -0
  39. pygpt_net/provider/api/google/__init__.py +6 -5
  40. pygpt_net/provider/api/google/chat.py +3 -8
  41. pygpt_net/{controller/chat/handler/google_stream.py → provider/api/google/stream.py} +1 -1
  42. pygpt_net/provider/api/google/utils.py +185 -0
  43. pygpt_net/{controller/chat/handler → provider/api/langchain}/__init__.py +0 -0
  44. pygpt_net/{controller/chat/handler/langchain_stream.py → provider/api/langchain/stream.py} +1 -1
  45. pygpt_net/provider/api/llama_index/__init__.py +0 -0
  46. pygpt_net/{controller/chat/handler/llamaindex_stream.py → provider/api/llama_index/stream.py} +1 -1
  47. pygpt_net/provider/api/openai/__init__.py +7 -3
  48. pygpt_net/provider/api/openai/image.py +2 -2
  49. pygpt_net/provider/api/openai/responses.py +0 -0
  50. pygpt_net/{controller/chat/handler/openai_stream.py → provider/api/openai/stream.py} +1 -1
  51. pygpt_net/provider/api/openai/utils.py +69 -3
  52. pygpt_net/provider/api/x_ai/__init__.py +117 -17
  53. pygpt_net/provider/api/x_ai/chat.py +272 -102
  54. pygpt_net/provider/api/x_ai/image.py +149 -47
  55. pygpt_net/provider/api/x_ai/{remote.py → remote_tools.py} +165 -70
  56. pygpt_net/provider/api/x_ai/responses.py +507 -0
  57. pygpt_net/provider/api/x_ai/stream.py +715 -0
  58. pygpt_net/provider/api/x_ai/tools.py +59 -8
  59. pygpt_net/{controller/chat/handler → provider/api/x_ai}/utils.py +1 -2
  60. pygpt_net/provider/api/x_ai/vision.py +1 -4
  61. pygpt_net/provider/core/config/patch.py +22 -1
  62. pygpt_net/provider/core/model/patch.py +26 -1
  63. pygpt_net/tools/image_viewer/ui/dialogs.py +300 -13
  64. pygpt_net/tools/text_editor/ui/dialogs.py +3 -2
  65. pygpt_net/tools/text_editor/ui/widgets.py +5 -1
  66. pygpt_net/ui/base/context_menu.py +44 -1
  67. pygpt_net/ui/layout/toolbox/indexes.py +22 -19
  68. pygpt_net/ui/layout/toolbox/model.py +28 -5
  69. pygpt_net/ui/widget/dialog/base.py +16 -5
  70. pygpt_net/ui/widget/image/display.py +25 -8
  71. pygpt_net/ui/widget/tabs/output.py +9 -1
  72. pygpt_net/ui/widget/textarea/editor.py +14 -1
  73. pygpt_net/ui/widget/textarea/input.py +20 -7
  74. pygpt_net/ui/widget/textarea/notepad.py +24 -1
  75. pygpt_net/ui/widget/textarea/output.py +23 -1
  76. pygpt_net/ui/widget/textarea/web.py +16 -1
  77. {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.7.dist-info}/METADATA +16 -2
  78. {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.7.dist-info}/RECORD +80 -73
  79. pygpt_net/controller/chat/handler/xai_stream.py +0 -135
  80. {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.7.dist-info}/LICENSE +0 -0
  81. {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.7.dist-info}/WHEEL +0 -0
  82. {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.7.dist-info}/entry_points.txt +0 -0
@@ -6,8 +6,9 @@
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: 2026.01.03 00:00:00 #
10
10
  # ================================================== #
11
+ from typing import Union
11
12
 
12
13
  from PySide6.QtWidgets import QMenu
13
14
  from PySide6.QtGui import QAction, QIcon
@@ -24,6 +25,8 @@ class ContextMenu:
24
25
  _ICON_CODE = QIcon(":/icons/code.svg")
25
26
  _ICON_TEXT = QIcon(":/icons/text.svg")
26
27
  _ICON_TRANSLATOR = QIcon(":/icons/translate.svg")
28
+ _ICON_ZOOM_IN = QIcon(":/icons/zoom_in.svg")
29
+ _ICON_ZOOM_OUT = QIcon(":/icons/zoom_out.svg")
27
30
 
28
31
  def __init__(self, window=None):
29
32
  """
@@ -33,6 +36,46 @@ class ContextMenu:
33
36
  """
34
37
  self.window = window
35
38
 
39
+ def get_zoom_menu(self, parent, parent_type: str, current_zoom: Union[int, float], callback = None) -> QMenu:
40
+ """
41
+ Get zoom menu (Zoom In/Out)
42
+
43
+ :param parent: Parent menu
44
+ :param parent_type: Type of textarea ('chat', 'notepad', etc.)
45
+ :param current_zoom: Current zoom level
46
+ :param callback: Callback function on zoom change
47
+ :return: Menu
48
+ """
49
+ menu = QMenu(trans('context_menu.zoom'), parent)
50
+ if parent_type in ("font_size.input", "font_size", "editor"):
51
+ action_zoom_in = QAction(self._ICON_ZOOM_IN, trans('context_menu.zoom.in'), menu)
52
+ new_zoom = current_zoom + 2
53
+ action_zoom_in.triggered.connect(
54
+ lambda checked=False, new_zoom=new_zoom: callback(new_zoom)
55
+ )
56
+ menu.addAction(action_zoom_in)
57
+ action_zoom_out = QAction(self._ICON_ZOOM_OUT, trans('context_menu.zoom.out'), menu)
58
+ new_zoom = current_zoom - 2
59
+ action_zoom_out.triggered.connect(
60
+ lambda checked=False, new_zoom=new_zoom: callback(new_zoom)
61
+ )
62
+ menu.addAction(action_zoom_out)
63
+ elif parent_type in ("zoom"):
64
+ action_zoom_in = QAction(self._ICON_ZOOM_IN, trans('context_menu.zoom.in'), menu)
65
+ new_zoom = current_zoom + 0.1
66
+ action_zoom_in.triggered.connect(
67
+ lambda checked=False, new_zoom=new_zoom: callback(new_zoom)
68
+ )
69
+ menu.addAction(action_zoom_in)
70
+ action_zoom_out = QAction(self._ICON_ZOOM_OUT, trans('context_menu.zoom.out'), menu)
71
+ new_zoom = current_zoom - 0.1
72
+ action_zoom_out.triggered.connect(
73
+ lambda checked=False, new_zoom=new_zoom: callback(new_zoom)
74
+ )
75
+ menu.addAction(action_zoom_out)
76
+
77
+ return menu
78
+
36
79
  def get_copy_to_menu(
37
80
  self,
38
81
  parent,
@@ -6,10 +6,11 @@
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: 2026.01.03 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from PySide6.QtGui import QStandardItemModel, Qt, QIcon
12
+ from PySide6.QtCore import Qt, QSize
13
+ from PySide6.QtGui import QStandardItemModel, QIcon
13
14
  from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QPushButton, QWidget, QCheckBox, QSizePolicy
14
15
 
15
16
  from pygpt_net.ui.widget.element.labels import HelpLabel, TitleLabel
@@ -54,6 +55,14 @@ class Indexes:
54
55
  """
55
56
  nodes = self.window.ui.nodes
56
57
  nodes['indexes.new'] = QPushButton(self._settings_icon, "")
58
+ # Configure compact, borderless settings button aligned to the right
59
+ icon_size = 20
60
+ nodes['indexes.new'].setFlat(True)
61
+ nodes['indexes.new'].setStyleSheet("QPushButton { border: none; background: transparent; padding: 0; }")
62
+ nodes['indexes.new'].setIconSize(QSize(icon_size, icon_size))
63
+ nodes['indexes.new'].setFixedSize(icon_size, icon_size)
64
+ nodes['indexes.new'].setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
65
+ nodes['indexes.new'].setFocusPolicy(Qt.NoFocus)
57
66
  nodes['indexes.new'].clicked.connect(self._open_llama_index_settings)
58
67
 
59
68
  nodes['indexes.label'] = TitleLabel(trans("toolbox.indexes.label"))
@@ -119,6 +128,14 @@ class Indexes:
119
128
  nodes['llama_index.mode.select'].setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
120
129
 
121
130
  nodes['indexes.new'] = QPushButton(self._settings_icon, "")
131
+ # Configure compact, borderless settings button for options row
132
+ icon_size = 20
133
+ nodes['indexes.new'].setFlat(True)
134
+ nodes['indexes.new'].setStyleSheet("QPushButton { border: none; background: transparent; padding: 0; }")
135
+ nodes['indexes.new'].setIconSize(QSize(icon_size, icon_size))
136
+ nodes['indexes.new'].setFixedSize(icon_size, icon_size)
137
+ nodes['indexes.new'].setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
138
+ nodes['indexes.new'].setFocusPolicy(Qt.NoFocus)
122
139
  nodes['indexes.new'].clicked.connect(self._open_llama_index_settings)
123
140
 
124
141
  nodes['indexes.label'] = TitleLabel(trans("toolbox.indexes.label"))
@@ -129,6 +146,8 @@ class Indexes:
129
146
  idx_layout.addWidget(nodes['indexes.select'])
130
147
  idx_layout.addWidget(nodes['indexes.new'], alignment=Qt.AlignRight)
131
148
  idx_layout.setContentsMargins(0, 0, 0, 10)
149
+ # Ensure the combo takes maximum horizontal space
150
+ idx_layout.setStretch(1, 1)
132
151
  idx_widget = QWidget()
133
152
  idx_widget.setLayout(idx_layout)
134
153
  idx_widget.setMinimumHeight(55)
@@ -138,6 +157,7 @@ class Indexes:
138
157
  mode_layout.addWidget(nodes['llama_index.mode.label'])
139
158
  mode_layout.addWidget(nodes['llama_index.mode.select'])
140
159
  mode_layout.setContentsMargins(0, 0, 0, 10)
160
+ mode_layout.setStretch(1, 1)
141
161
  mode_widget = QWidget()
142
162
  mode_widget.setLayout(mode_layout)
143
163
  mode_widget.setMinimumHeight(55)
@@ -173,23 +193,6 @@ class Indexes:
173
193
  if self._last_combo_signature != signature:
174
194
  self.window.ui.nodes['indexes.select'].set_keys(combo_keys)
175
195
  self._last_combo_signature = signature
176
- """
177
- # store previous selection
178
- self.window.ui.nodes[self.id].backup_selection()
179
-
180
- self.window.ui.models[self.id].removeRows(0, self.window.ui.models[self.id].rowCount())
181
- i = 0
182
- for item in data:
183
- self.window.ui.models[self.id].insertRow(i)
184
- name = item['name']
185
- index = self.window.ui.models[self.id].index(i, 0)
186
- self.window.ui.models[self.id].setData(index, "ID: " + item['id'], QtCore.Qt.ToolTipRole)
187
- self.window.ui.models[self.id].setData(self.window.ui.models[self.id].index(i, 0), name)
188
- i += 1
189
-
190
- # restore previous selection
191
- self.window.ui.nodes[self.id].restore_selection()
192
- """
193
196
 
194
197
  def _open_llama_index_settings(self):
195
198
  self.window.controller.settings.open_section('llama-index')
@@ -6,10 +6,11 @@
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: 2026.01.03 17:00:00 #
10
10
  # ================================================== #
11
-
12
- from PySide6.QtWidgets import QVBoxLayout, QWidget
11
+ from PySide6.QtCore import Qt, QSize
12
+ from PySide6.QtGui import QIcon
13
+ from PySide6.QtWidgets import QVBoxLayout, QWidget, QPushButton, QHBoxLayout, QSizePolicy
13
14
 
14
15
  from pygpt_net.ui.widget.element.labels import TitleLabel
15
16
  from pygpt_net.ui.widget.lists.model_combo import ModelCombo
@@ -27,6 +28,7 @@ class Model:
27
28
  self.window = window
28
29
  self.id = 'prompt.model'
29
30
  self.label_key = f'{self.id}.label'
31
+ self._settings_icon = QIcon(":/icons/settings.svg")
30
32
 
31
33
  def setup(self) -> QWidget:
32
34
  """
@@ -51,12 +53,33 @@ class Model:
51
53
  nodes[self.label_key] = label
52
54
 
53
55
  combo = ModelCombo(self.window, self.id)
56
+ # Ensure combo takes maximum horizontal space
57
+ combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
54
58
  nodes[self.id] = combo
55
59
 
60
+ nodes['prompt.model.settings'] = QPushButton(self._settings_icon, "")
61
+ # Configure compact, borderless settings button aligned to the right
62
+ icon_size = 20
63
+ nodes['prompt.model.settings'].setFlat(True)
64
+ nodes['prompt.model.settings'].setStyleSheet("QPushButton { border: none; padding: 0; }")
65
+ nodes['prompt.model.settings'].setIconSize(QSize(icon_size, icon_size))
66
+ nodes['prompt.model.settings'].setFixedSize(icon_size, icon_size)
67
+ nodes['prompt.model.settings'].setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
68
+ nodes['prompt.model.settings'].setFocusPolicy(Qt.NoFocus)
69
+ nodes['prompt.model.settings'].clicked.connect(self._open_settings)
70
+
71
+ model_cols = QHBoxLayout()
72
+ model_cols.addWidget(combo, 1) # stretch to take remaining space
73
+ model_cols.addWidget(nodes['prompt.model.settings'], alignment=Qt.AlignRight)
74
+ model_cols.setContentsMargins(0, 0, 0, 0)
75
+
56
76
  layout = QVBoxLayout()
57
77
  layout.addWidget(label)
58
- layout.addWidget(combo)
78
+ layout.addLayout(model_cols)
59
79
  layout.addStretch()
60
80
  layout.setContentsMargins(2, 5, 5, 5)
61
81
 
62
- return layout
82
+ return layout
83
+
84
+ def _open_settings(self):
85
+ self.window.controller.model.editor.open()
@@ -6,11 +6,11 @@
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.12.31 14:00:00 #
9
+ # Updated Date: 2026.01.05 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtWidgets import QApplication
13
- from PySide6.QtCore import QRect, QSize, QPoint, QEvent, Qt
13
+ from PySide6.QtCore import QSize, QPoint, QEvent, Qt
14
14
  from PySide6.QtWidgets import QDialog
15
15
 
16
16
 
@@ -25,11 +25,20 @@ class BaseDialog(QDialog):
25
25
  super(BaseDialog, self).__init__(window)
26
26
  self.window = window
27
27
  self.id = id
28
+ self.shared_id = None
28
29
  self.disable_geometry_store = False
29
30
  self.setWindowFlags(
30
31
  self.windowFlags() | Qt.WindowMaximizeButtonHint
31
32
  )
32
33
 
34
+ def _get_id(self):
35
+ """
36
+ Get dialog id
37
+
38
+ :return: dialog id
39
+ """
40
+ return self.shared_id if self.shared_id is not None else self.id
41
+
33
42
  def showEvent(self, event):
34
43
  """
35
44
  Event called when the dialog is shown
@@ -58,7 +67,7 @@ class BaseDialog(QDialog):
58
67
  if self.disable_geometry_store:
59
68
  return False
60
69
 
61
- if self.window is None or self.id is None:
70
+ if self.window is None or self._get_id() is None:
62
71
  return False
63
72
 
64
73
  config = self.window.core.config
@@ -71,6 +80,7 @@ class BaseDialog(QDialog):
71
80
  def save_geometry(self):
72
81
  """Save dialog geometry"""
73
82
  config = self.window.core.config
83
+ id = self._get_id()
74
84
  item = {
75
85
  "size": [self.size().width(), self.size().height()],
76
86
  "pos": [self.pos().x(), self.pos().y()]
@@ -81,7 +91,7 @@ class BaseDialog(QDialog):
81
91
 
82
92
  if not isinstance(data, dict):
83
93
  data = {}
84
- data[self.id] = item
94
+ data[id] = item
85
95
 
86
96
  if self.store_geometry_enabled():
87
97
  config.set_session("layout.dialog.geometry", data)
@@ -89,6 +99,7 @@ class BaseDialog(QDialog):
89
99
  def restore_geometry(self):
90
100
  """Restore dialog geometry"""
91
101
  # get available screen geometry
102
+ id = self._get_id()
92
103
  screen = QApplication.primaryScreen()
93
104
  available_geometry = screen.availableGeometry()
94
105
  config = self.window.core.config
@@ -97,7 +108,7 @@ class BaseDialog(QDialog):
97
108
  if self.store_geometry_enabled():
98
109
  data = config.get_session("layout.dialog.geometry", {})
99
110
 
100
- item = data.get(self.id, {})
111
+ item = data.get(id, {})
101
112
  if isinstance(item, dict) and "size" in item and "pos" in item:
102
113
  width, height = item["size"]
103
114
  x, y = item["pos"]
@@ -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: 2026.01.03 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtGui import QAction, QIcon
@@ -27,6 +27,17 @@ class ImageLabel(QLabel):
27
27
  self.window = window
28
28
  self.path = path
29
29
 
30
+ def _get_window(self):
31
+ """
32
+ Get main window instance
33
+
34
+ :return: Window instance
35
+ """
36
+ if self.window is not None:
37
+ if hasattr(self.window, 'window'):
38
+ return self.window.window
39
+ return self.window
40
+
30
41
  def contextMenuEvent(self, event):
31
42
  """
32
43
  Context menu event
@@ -36,6 +47,8 @@ class ImageLabel(QLabel):
36
47
  if not self.path:
37
48
  return
38
49
 
50
+ win = self._get_window()
51
+
39
52
  actions = {}
40
53
  use_actions = []
41
54
  actions['open'] = QAction(QIcon(":/icons/fullscreen.svg"), trans('img.action.open'), self)
@@ -64,13 +77,13 @@ class ImageLabel(QLabel):
64
77
  self,
65
78
  )
66
79
  actions['use_attachment'].triggered.connect(
67
- lambda: self.window.controller.files.use_attachment(self.path),
80
+ lambda: win.controller.files.use_attachment(self.path),
68
81
  )
69
82
  use_actions.append(actions['use_attachment'])
70
83
 
71
84
  # use by filetype
72
- if self.window.core.filesystem.actions.has_use(self.path):
73
- extra_use_actions = self.window.core.filesystem.actions.get_use(self, self.path)
85
+ if win.core.filesystem.actions.has_use(self.path):
86
+ extra_use_actions = win.core.filesystem.actions.get_use(self, self.path)
74
87
  for action in extra_use_actions:
75
88
  use_actions.append(action)
76
89
 
@@ -97,7 +110,8 @@ class ImageLabel(QLabel):
97
110
 
98
111
  :param event: mouse event
99
112
  """
100
- self.window.tools.get("viewer").open(self.path)
113
+ win = self._get_window()
114
+ win.tools.get("viewer").open(self.path)
101
115
 
102
116
  def action_open_dir(self, event):
103
117
  """
@@ -105,7 +119,8 @@ class ImageLabel(QLabel):
105
119
 
106
120
  :param event: mouse event
107
121
  """
108
- self.window.tools.get("viewer").open_dir(self.path)
122
+ win = self._get_window()
123
+ win.tools.get("viewer").open_dir(self.path)
109
124
 
110
125
  def action_save(self, event):
111
126
  """
@@ -113,7 +128,8 @@ class ImageLabel(QLabel):
113
128
 
114
129
  :param event: mouse event
115
130
  """
116
- self.window.tools.get("viewer").save(self.path)
131
+ win = self._get_window()
132
+ win.tools.get("viewer").save(self.path)
117
133
 
118
134
  def action_delete(self, event):
119
135
  """
@@ -121,4 +137,5 @@ class ImageLabel(QLabel):
121
137
 
122
138
  :param event: mouse event
123
139
  """
124
- self.window.tools.get("viewer").delete(self.path)
140
+ win = self._get_window()
141
+ win.tools.get("viewer").delete(self.path)
@@ -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.25 18:00:00 #
9
+ # Updated Date: 2026.01.03 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtWidgets import QTabWidget, QMenu, QPushButton, QToolButton, QTabBar
@@ -502,6 +502,14 @@ class OutputTabs(QTabWidget):
502
502
  self.show_tool_menu(idx, column_idx, event.globalPos()) # tool
503
503
  else:
504
504
  self.show_default_menu(idx, column_idx, event.globalPos()) # default
505
+
506
+ # close on middle click
507
+ elif event.button() == Qt.MiddleButton:
508
+ idx = self.tabBar().tabAt(event.pos())
509
+ column_idx = self.column.get_idx()
510
+ self.window.controller.ui.tabs.close(idx, column_idx)
511
+ event.accept()
512
+ return
505
513
  super(OutputTabs, self).mousePressEvent(event)
506
514
 
507
515
  def prepare_menu(self, index: int, column_idx: int) -> QMenu:
@@ -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: 2026.01.03 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import Qt
@@ -88,6 +88,10 @@ class BaseCodeEditor(QTextEdit):
88
88
  lambda: self.window.controller.chat.common.save_text(self.toPlainText()))
89
89
  menu.addAction(action)
90
90
 
91
+ # Add zoom submenu
92
+ zoom_menu = self.window.ui.context_menu.get_zoom_menu(self, "editor", self.value, self.on_zoom_changed)
93
+ menu.addMenu(zoom_menu)
94
+
91
95
  action = QAction(self._ICON_SEARCH, trans('text.context_menu.find'), menu)
92
96
  action.triggered.connect(self.find_open)
93
97
  action.setShortcut(self._FIND_SEQ)
@@ -137,6 +141,15 @@ class BaseCodeEditor(QTextEdit):
137
141
  else:
138
142
  super().wheelEvent(event)
139
143
 
144
+ def on_zoom_changed(self, value: int):
145
+ """
146
+ On font size changed
147
+
148
+ :param value: New font size
149
+ """
150
+ self.value = value
151
+ self.update_stylesheet(f"QTextEdit {{ font-size: {value}px }};")
152
+
140
153
  def focusInEvent(self, e):
141
154
  super().focusInEvent(e)
142
155
  self.window.controller.finder.focus_in(self.finder)
@@ -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.28 08:00:00 #
9
+ # Updated Date: 2026.01.03 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Optional
@@ -394,6 +394,10 @@ class ChatInput(QTextEdit):
394
394
  action.triggered.connect(lambda: self.window.controller.chat.common.save_text(self.toPlainText()))
395
395
  menu.addAction(action)
396
396
 
397
+ # Add zoom submenu
398
+ zoom_menu = self.window.ui.context_menu.get_zoom_menu(self, "font_size.input", self.value, self.on_zoom_changed)
399
+ menu.addMenu(zoom_menu)
400
+
397
401
  try:
398
402
  self.window.core.prompt.template.to_menu_options(menu, "input")
399
403
  self.window.core.prompt.custom.to_menu_options(menu, "input")
@@ -458,8 +462,8 @@ class ChatInput(QTextEdit):
458
462
  :param event: Event
459
463
  """
460
464
  if event.modifiers() & Qt.ControlModifier:
461
- prev = self.value
462
465
  dy = event.angleDelta().y()
466
+ prev = self.value
463
467
  if dy > 0:
464
468
  if self.value < self.max_font_size:
465
469
  self.value += 1
@@ -468,15 +472,24 @@ class ChatInput(QTextEdit):
468
472
  self.value -= 1
469
473
 
470
474
  if self.value != prev:
471
- self.window.core.config.data['font_size.input'] = self.value
472
- self.window.core.config.save()
473
- self.window.controller.ui.update_font_size()
474
- # Reflow may change number of lines; adjust auto-height next tick
475
- QTimer.singleShot(0, self._schedule_auto_resize)
475
+ self.on_zoom_changed(self.value)
476
476
  event.accept()
477
477
  return
478
478
  super().wheelEvent(event)
479
479
 
480
+ def on_zoom_changed(self, value: int):
481
+ """
482
+ Called when zoom level changes.
483
+
484
+ :param value: new zoom level
485
+ """
486
+ self.value = value
487
+ self.window.core.config.data['font_size.input'] = value
488
+ self.window.core.config.save()
489
+ self.window.controller.ui.update_font_size()
490
+ # Reflow may change number of lines; adjust auto-height next tick
491
+ QTimer.singleShot(0, self._schedule_auto_resize)
492
+
480
493
  def changeEvent(self, event):
481
494
  super().changeEvent(event)
482
495
  if event.type() == QEvent.FontChange:
@@ -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: 2026.01.03 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import Qt, QEvent, QTimer
@@ -214,6 +214,10 @@ class NotepadOutput(QTextEdit):
214
214
  lambda: self.window.controller.chat.common.save_text(self.toPlainText()))
215
215
  menu.addAction(action)
216
216
 
217
+ # Add zoom submenu
218
+ zoom_menu = self.window.ui.context_menu.get_zoom_menu(self, "font_size", self.value, self.on_zoom_changed)
219
+ menu.addMenu(zoom_menu)
220
+
217
221
  action = QAction(self.ICON_SEARCH, trans('text.context_menu.find'), self)
218
222
  action.triggered.connect(self.find_open)
219
223
  action.setShortcut(QKeySequence("Ctrl+F"))
@@ -282,6 +286,25 @@ class NotepadOutput(QTextEdit):
282
286
  super(NotepadOutput, self).wheelEvent(event)
283
287
  self.last_scroll_pos = self._vscroll.value()
284
288
 
289
+ def on_zoom_changed(self, value: int):
290
+ """
291
+ On font size changed
292
+
293
+ :param value: New font size
294
+ """
295
+ self.value = value
296
+ self.window.core.config.data['font_size'] = value
297
+ self.window.core.config.save()
298
+ option = self.window.controller.settings.editor.get_option('font_size')
299
+ option['value'] = self.value
300
+ self.window.controller.config.apply(
301
+ parent_id='config',
302
+ key='font_size',
303
+ option=option,
304
+ )
305
+ self.window.controller.ui.update_font_size()
306
+ self.last_scroll_pos = self._vscroll.value()
307
+
285
308
  def focusInEvent(self, e):
286
309
  """
287
310
  Focus in event
@@ -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: 2026.01.03 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import Qt, QEvent
@@ -118,6 +118,10 @@ class ChatOutput(QTextBrowser):
118
118
  action.triggered.connect(self._save_all_text)
119
119
  menu.addAction(action)
120
120
 
121
+ # Add zoom submenu
122
+ zoom_menu = self.window.ui.context_menu.get_zoom_menu(self, "font_size", self.value, self.on_zoom_changed)
123
+ menu.addMenu(zoom_menu)
124
+
121
125
  action = QAction(self.ICON_SEARCH, trans('text.context_menu.find'), self)
122
126
  action.triggered.connect(self.find_open)
123
127
  action.setShortcut(QKeySequence("Ctrl+F"))
@@ -193,6 +197,24 @@ class ChatOutput(QTextBrowser):
193
197
  else:
194
198
  super().wheelEvent(event)
195
199
 
200
+ def on_zoom_changed(self, value: int):
201
+ """
202
+ On font size changed
203
+
204
+ :param value: New font size
205
+ """
206
+ self.value = value
207
+
208
+ cfg = self.window.core.config
209
+ cfg.data['font_size'] = value
210
+ cfg.save()
211
+
212
+ ctrl = self.window.controller
213
+ option = ctrl.settings.editor.get_option('font_size')
214
+ option['value'] = value
215
+ ctrl.config.apply(parent_id='config', key='font_size', option=option)
216
+ ctrl.ui.update_font_size()
217
+
196
218
  def focusInEvent(self, e):
197
219
  """
198
220
  Focus in event
@@ -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: 2026.01.03 00:00:00 #
10
10
  # ================================================== #
11
11
  from PySide6 import QtCore
12
12
  from PySide6.QtCore import Qt, QObject, Signal, Slot, QEvent, QUrl, QCoreApplication, QEventLoop
@@ -345,12 +345,27 @@ class ChatWebOutput(QWebEngineView):
345
345
  action.triggered.connect(self._save_as_html)
346
346
  menu.addAction(action)
347
347
 
348
+ # Add zoom submenu
349
+ zoom_menu = self.window.ui.context_menu.get_zoom_menu(self, "zoom", self.get_zoom_value(), self.on_zoom_changed)
350
+ menu.addMenu(zoom_menu)
351
+
348
352
  action = QAction(QIcon(":/icons/search.svg"), trans('text.context_menu.find'), self)
349
353
  action.triggered.connect(self.find_open)
350
354
  menu.addAction(action)
351
355
 
352
356
  menu.exec_(self.mapToGlobal(position))
353
357
 
358
+ def on_zoom_changed(self, zoom: float):
359
+ """
360
+ On zoom changed from context menu
361
+
362
+ :param zoom: float - new zoom factor
363
+ """
364
+ p = self.page()
365
+ if p:
366
+ self.window.core.config.set("zoom", zoom)
367
+ self.update_zoom()
368
+
354
369
  @Slot()
355
370
  def _save_selected_txt(self):
356
371
  """Save selected content as text file"""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pygpt-net
3
- Version: 2.7.5
3
+ Version: 2.7.7
4
4
  Summary: Desktop AI Assistant powered by: OpenAI GPT-5, GPT-4, o1, o3, Gemini, Claude, Grok, DeepSeek, and other models supported by Llama Index, and Ollama. Chatbot, agents, completion, image generation, vision analysis, speech-to-text, plugins, MCP, internet access, file handling, command execution and more.
5
5
  License: MIT
6
6
  Keywords: ai,api,api key,app,assistant,bielik,chat,chatbot,chatgpt,claude,dall-e,deepseek,desktop,gemini,gpt,gpt-3.5,gpt-4,gpt-4-vision,gpt-4o,gpt-5,gpt-oss,gpt3.5,gpt4,grok,langchain,llama-index,llama3,mistral,o1,o3,ollama,openai,presets,py-gpt,py_gpt,pygpt,pyside,qt,text completion,tts,ui,vision,whisper
@@ -120,7 +120,7 @@ Description-Content-Type: text/markdown
120
120
 
121
121
  [![pygpt](https://snapcraft.io/pygpt/badge.svg)](https://snapcraft.io/pygpt)
122
122
 
123
- Release: **2.7.5** | build: **2026-01-03** | Python: **>=3.10, <3.14**
123
+ Release: **2.7.7** | build: **2026-01-05** | Python: **>=3.10, <3.14**
124
124
 
125
125
  > Official website: https://pygpt.net | Documentation: https://pygpt.readthedocs.io
126
126
  >
@@ -3796,6 +3796,20 @@ may consume additional tokens that are not displayed in the main window.
3796
3796
 
3797
3797
  ## Recent changes:
3798
3798
 
3799
+ **2.7.7 (2026-01-05)**
3800
+
3801
+ - Added support for Responses API in xAI.
3802
+ - Added xAI remote tools: Remote MCP, Code Execution.
3803
+ - Added Anthropic remote tools: Remote MCP, Web Fetch, Code Execution.
3804
+
3805
+ **2.7.6 (2026-01-03)**
3806
+
3807
+ - Fixed compatibility with xAI SDK and resolved empty responses from Grok models.
3808
+ - Fixed missing libraries in the Snap package.
3809
+ - Added zoom and grab functionality in the Image Viewer.
3810
+ - Added a zoom menu to textarea and web widgets.
3811
+ - Added the ability to close tabs with a middle mouse button click.
3812
+
3799
3813
  **2.7.5 (2026-01-03)**
3800
3814
 
3801
3815
  - Added Sandbox/Playwright option to Computer Use mode.