pygpt-net 2.6.60__py3-none-any.whl → 2.6.61__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 (60) hide show
  1. pygpt_net/CHANGELOG.txt +7 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/chat/common.py +115 -6
  4. pygpt_net/controller/chat/input.py +4 -1
  5. pygpt_net/controller/presets/presets.py +121 -6
  6. pygpt_net/controller/settings/editor.py +0 -15
  7. pygpt_net/controller/theme/markdown.py +2 -5
  8. pygpt_net/controller/ui/ui.py +4 -7
  9. pygpt_net/core/agents/custom/__init__.py +7 -1
  10. pygpt_net/core/agents/custom/llama_index/factory.py +17 -6
  11. pygpt_net/core/agents/custom/llama_index/runner.py +35 -2
  12. pygpt_net/core/agents/custom/llama_index/utils.py +12 -1
  13. pygpt_net/core/agents/custom/router.py +45 -6
  14. pygpt_net/core/agents/custom/runner.py +2 -1
  15. pygpt_net/core/agents/custom/schema.py +3 -1
  16. pygpt_net/core/agents/custom/utils.py +13 -1
  17. pygpt_net/core/db/viewer.py +11 -5
  18. pygpt_net/core/node_editor/graph.py +18 -9
  19. pygpt_net/core/node_editor/models.py +9 -2
  20. pygpt_net/core/node_editor/types.py +3 -1
  21. pygpt_net/core/presets/presets.py +216 -29
  22. pygpt_net/core/render/markdown/parser.py +0 -2
  23. pygpt_net/data/config/config.json +5 -6
  24. pygpt_net/data/config/models.json +3 -3
  25. pygpt_net/data/config/settings.json +2 -38
  26. pygpt_net/data/locale/locale.de.ini +64 -1
  27. pygpt_net/data/locale/locale.en.ini +62 -3
  28. pygpt_net/data/locale/locale.es.ini +64 -1
  29. pygpt_net/data/locale/locale.fr.ini +64 -1
  30. pygpt_net/data/locale/locale.it.ini +64 -1
  31. pygpt_net/data/locale/locale.pl.ini +65 -2
  32. pygpt_net/data/locale/locale.uk.ini +64 -1
  33. pygpt_net/data/locale/locale.zh.ini +64 -1
  34. pygpt_net/data/locale/plugin.cmd_system.en.ini +62 -66
  35. pygpt_net/provider/agents/llama_index/flow_from_schema.py +2 -2
  36. pygpt_net/provider/core/config/patch.py +10 -1
  37. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +0 -6
  38. pygpt_net/tools/agent_builder/tool.py +42 -26
  39. pygpt_net/tools/agent_builder/ui/dialogs.py +60 -11
  40. pygpt_net/ui/__init__.py +2 -4
  41. pygpt_net/ui/dialog/about.py +58 -38
  42. pygpt_net/ui/dialog/db.py +142 -3
  43. pygpt_net/ui/dialog/preset.py +47 -8
  44. pygpt_net/ui/layout/toolbox/presets.py +52 -16
  45. pygpt_net/ui/widget/dialog/db.py +0 -0
  46. pygpt_net/ui/widget/lists/preset.py +644 -60
  47. pygpt_net/ui/widget/node_editor/command.py +10 -10
  48. pygpt_net/ui/widget/node_editor/config.py +157 -0
  49. pygpt_net/ui/widget/node_editor/editor.py +183 -151
  50. pygpt_net/ui/widget/node_editor/item.py +12 -11
  51. pygpt_net/ui/widget/node_editor/node.py +267 -12
  52. pygpt_net/ui/widget/node_editor/view.py +180 -63
  53. pygpt_net/ui/widget/tabs/output.py +1 -1
  54. pygpt_net/ui/widget/textarea/input.py +2 -2
  55. pygpt_net/utils.py +114 -2
  56. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/METADATA +11 -94
  57. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/RECORD +59 -58
  58. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/LICENSE +0 -0
  59. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/WHEEL +0 -0
  60. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/entry_points.txt +0 -0
@@ -11,7 +11,7 @@
11
11
 
12
12
  from PySide6.QtCore import Qt, QEvent
13
13
  from PySide6.QtGui import QAction, QIcon
14
- from PySide6.QtWidgets import QVBoxLayout, QMenuBar, QSplitter, QSizePolicy
14
+ from PySide6.QtWidgets import QVBoxLayout, QMenuBar, QSplitter, QSizePolicy, QWidget
15
15
 
16
16
  from pygpt_net.ui.widget.element.labels import HelpLabel
17
17
  from pygpt_net.ui.widget.node_editor.editor import NodeEditor
@@ -41,13 +41,13 @@ class Builder:
41
41
  self.file_menu = self.menu_bar.addMenu(trans("menu.file"))
42
42
  t = self.tool
43
43
 
44
- self.actions["open"] = QAction(QIcon(":/icons/reload.svg"), "Reload", self.menu_bar)
44
+ self.actions["open"] = QAction(QIcon(":/icons/reload.svg"), trans("action.reload"), self.menu_bar)
45
45
  self.actions["open"].triggered.connect(lambda checked=False, t=t: t.load())
46
46
 
47
47
  self.actions["save"] = QAction(QIcon(":/icons/save.svg"), trans("action.save"), self.menu_bar)
48
48
  self.actions["save"].triggered.connect(lambda checked=False, t=t: t.save())
49
49
 
50
- self.actions["clear"] = QAction(QIcon(":/icons/clear.svg"), trans("action.clear"), self.menu_bar)
50
+ self.actions["clear"] = QAction(QIcon(":/icons/close.svg"), trans("action.clear"), self.menu_bar)
51
51
  self.actions["clear"].triggered.connect(lambda checked=False, t=t: t.clear())
52
52
 
53
53
  self.file_menu.addAction(self.actions["open"])
@@ -77,7 +77,28 @@ class Builder:
77
77
  registry=registry
78
78
  ) # parent == dialog
79
79
 
80
- editor.setStyleSheet("""
80
+ theme = self.window.core.config.get("theme")
81
+ if theme.startswith("light"):
82
+ style = """
83
+ NodeEditor {
84
+ qproperty-gridBackColor: #ffffff;
85
+ qproperty-gridPenColor: #eaeaea;
86
+
87
+ qproperty-nodeBackgroundColor: #2d2f34;
88
+ qproperty-nodeBorderColor: #4b4f57;
89
+ qproperty-nodeSelectionColor: #ff9900;
90
+ qproperty-nodeTitleColor: #3a3d44;
91
+
92
+ qproperty-portInputColor: #66b2ff;
93
+ qproperty-portOutputColor: #70e070;
94
+ qproperty-portConnectedColor: #ffd166;
95
+
96
+ qproperty-edgeColor: #c0c0c0;
97
+ qproperty-edgeSelectedColor: #ff8a5c;
98
+ }
99
+ """
100
+ else:
101
+ style = """
81
102
  NodeEditor {
82
103
  qproperty-gridBackColor: #242629;
83
104
  qproperty-gridPenColor: #3b3f46;
@@ -94,8 +115,10 @@ class Builder:
94
115
  qproperty-edgeColor: #c0c0c0;
95
116
  qproperty-edgeSelectedColor: #ff8a5c;
96
117
  }
97
- """)
118
+ """
119
+ editor.setStyleSheet(style)
98
120
  editor.on_clear = self.tool.clear
121
+ editor.editing_allowed = self.tool.editing_allowed
99
122
 
100
123
  u.editor[id] = editor
101
124
 
@@ -104,9 +127,28 @@ class Builder:
104
127
 
105
128
  agents_list = AgentsWidget(self.window, tool=self.tool, parent=dlg)
106
129
  list_widget = agents_list.setup()
107
- list_widget.setFixedWidth(250)
130
+
131
+ # Left side container: list fills all space, help label stays at the bottom
132
+ left_side = QWidget(dlg)
133
+ left_layout = QVBoxLayout(left_side)
134
+ left_layout.setContentsMargins(0, 0, 0, 0)
135
+ left_layout.setSpacing(6)
136
+
137
+ left_help_label = HelpLabel(trans("node.editor.list.tip"))
138
+ left_help_label.setWordWrap(True)
139
+ left_help_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
140
+ left_help_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
141
+ left_help_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
142
+
143
+ left_layout.addWidget(list_widget)
144
+ left_layout.addWidget(left_help_label)
145
+ left_layout.setStretch(0, 1) # list -> fills all vertical space
146
+
147
+ # Fix the width of the whole left panel (not only the list)
148
+ left_side.setFixedWidth(250)
149
+
108
150
  center_splitter = QSplitter(Qt.Horizontal)
109
- center_splitter.addWidget(list_widget)
151
+ center_splitter.addWidget(left_side)
110
152
  center_splitter.addWidget(u.editor[id])
111
153
  center_splitter.setStretchFactor(0, 1)
112
154
  center_splitter.setStretchFactor(1, 8)
@@ -115,10 +157,7 @@ class Builder:
115
157
  layout.setStretch(0, 1)
116
158
 
117
159
  # Bottom legend as a compact, centered help label
118
- legend_label = HelpLabel(
119
- "Right-click: add node / undo / redo • Middle-click: pan view • Ctrl + Mouse wheel: zoom • "
120
- "Left-click a port: create connection • Ctrl + Left-click a port: rewire or detach • Right-click or DEL a node/connection: remove"
121
- )
160
+ legend_label = HelpLabel(trans("node.editor.bottom.tip"))
122
161
  legend_label.setAlignment(Qt.AlignCenter)
123
162
  legend_label.setWordWrap(True)
124
163
  legend_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
@@ -129,6 +168,7 @@ class Builder:
129
168
  u.nodes["agent.builder.splitter"] = center_splitter
130
169
  u.nodes["agent.builder.list"] = agents_list
131
170
  u.nodes["agent.builder.legend"] = legend_label
171
+ u.nodes["agent.builder.list.help"] = left_help_label
132
172
 
133
173
  dlg.setLayout(layout)
134
174
  dlg.setWindowTitle(trans("agent.builder.title"))
@@ -231,6 +271,15 @@ class BuilderDialog(BaseDialog):
231
271
  except Exception:
232
272
  pass
233
273
 
274
+ # Dispose left-side help label safely
275
+ try:
276
+ help_lbl = u.nodes.pop("agent.builder.list.help", None)
277
+ if help_lbl is not None:
278
+ help_lbl.setParent(None)
279
+ help_lbl.deleteLater()
280
+ except Exception:
281
+ pass
282
+
234
283
  # Drop splitter reference
235
284
  try:
236
285
  u.nodes.pop("agent.builder.splitter", None)
pygpt_net/ui/__init__.py CHANGED
@@ -172,15 +172,13 @@ class UI:
172
172
 
173
173
  def show_loading(self):
174
174
  """Show loading"""
175
- if self.window.core.config.get('layout.animation.disable', False):
176
- return
175
+ return
177
176
  self.window.ui.nodes['anim.loading'].start_anim()
178
177
  self.window.ui.nodes['anim.loading'].show()
179
178
 
180
179
  def hide_loading(self):
181
180
  """Hide loading"""
182
- if self.window.core.config.get('layout.animation.disable', False):
183
- return
181
+ return
184
182
  self.window.ui.nodes['anim.loading'].stop_anim()
185
183
  self.window.ui.nodes['anim.loading'].hide()
186
184
 
@@ -19,6 +19,9 @@ from pygpt_net.utils import trans
19
19
 
20
20
 
21
21
  class About:
22
+
23
+ RELEASE_YEAR = 2025
24
+
22
25
  def __init__(self, window=None):
23
26
  """
24
27
  About dialog
@@ -36,34 +39,65 @@ class About:
36
39
  """
37
40
  return self.window.core.updater.get_fetch_thanks()
38
41
 
42
+ def build_versions_str(self, lib_versions: dict, break_after: str = "LlamaIndex") -> str:
43
+ parts = []
44
+ line = []
45
+ for k, v in lib_versions.items():
46
+ line.append(f"{k}: {v}")
47
+ if k == break_after:
48
+ parts.append(", ".join(line))
49
+ line = []
50
+ if line:
51
+ parts.append(", ".join(line))
52
+ return "\n".join(parts)
53
+
39
54
  def prepare_content(self) -> str:
40
55
  """
41
56
  Get info text
42
57
 
43
58
  :return: info text
44
59
  """
45
- llama_index_version = None
46
- langchain_version = None
47
- openai_version = None
48
- versions = True
60
+ lib_versions = {}
61
+
62
+ try:
63
+ import platform
64
+ lib_versions['Python'] = platform.python_version()
65
+ except ImportError:
66
+ pass
49
67
 
50
68
  try:
51
- from llama_index.core import __version__ as llama_index_version
52
- # from langchain import __version__ as langchain_version
53
69
  from openai.version import VERSION as openai_version
70
+ lib_versions['OpenAI API'] = openai_version
71
+ except ImportError:
72
+ pass
73
+
74
+ try:
75
+ from llama_index.core import __version__ as llama_index_version
76
+ lib_versions['LlamaIndex'] = llama_index_version
77
+ except ImportError:
78
+ pass
79
+
80
+ try:
81
+ from anthropic import __version__ as anthropic_version
82
+ lib_versions['Anthropic API'] = anthropic_version
83
+ except ImportError:
84
+ pass
85
+
86
+ try:
87
+ from google.genai import __version__ as google_genai_version
88
+ lib_versions['Google API'] = google_genai_version
54
89
  except ImportError:
55
90
  pass
56
91
 
57
- lib_versions = ""
58
- if llama_index_version is None or openai_version is None:
59
- versions = False
92
+ try:
93
+ from xai_sdk import __version__ as xai_sdk_version
94
+ lib_versions['xAI API'] = xai_sdk_version
95
+ except ImportError:
96
+ pass
60
97
 
61
- if versions:
62
- lib_versions = "OpenAI API: {}, LlamaIndex: {}\n\n".format(
63
- openai_version,
64
- # langchain_version,
65
- llama_index_version,
66
- )
98
+ versions_str = ""
99
+ if lib_versions:
100
+ versions_str = self.build_versions_str(lib_versions, break_after="LlamaIndex")
67
101
 
68
102
  platform = self.window.core.platforms.get_as_string()
69
103
  version = self.window.meta['version']
@@ -80,29 +114,14 @@ class About:
80
114
  label_github = trans("dialog.about.github")
81
115
  label_docs = trans("dialog.about.docs")
82
116
 
83
- data = "{label_version}: {version}, {platform}\n" \
84
- "{label_build}: {build}\n" \
85
- "{lib_versions}" \
86
- "{label_website}: {website}\n" \
87
- "{label_github}: {github}\n" \
88
- "{label_docs}: {docs}\n\n" \
89
- "(c) 2025 {author}\n" \
90
- "{email}\n".format(
91
- label_version=label_version,
92
- version=version,
93
- platform=platform,
94
- label_build=label_build,
95
- build=build.replace('.', '-'),
96
- label_website=label_website,
97
- website=website,
98
- label_github=label_github,
99
- github=github,
100
- label_docs=label_docs,
101
- docs=docs,
102
- author=author,
103
- email=email,
104
- lib_versions=lib_versions,
105
- )
117
+ data = f"{label_version}: {version}, {platform}\n" \
118
+ f"{label_build}: {build.replace('.', '-')}\n\n" \
119
+ f"{versions_str}\n\n" \
120
+ f"{label_website}: {website}\n" \
121
+ f"{label_github}: {github}\n" \
122
+ f"{label_docs}: {docs}\n\n" \
123
+ f"(c) {self.RELEASE_YEAR} {author}\n" \
124
+ f"{email}\n"
106
125
  return data
107
126
 
108
127
  def setup(self):
@@ -139,6 +158,7 @@ class About:
139
158
  string = self.prepare_content()
140
159
  content = QLabel(string)
141
160
  content.setTextInteractionFlags(Qt.TextSelectableByMouse)
161
+ content.setWordWrap(True)
142
162
  self.window.ui.nodes['dialog.about.content'] = content
143
163
 
144
164
  thanks = QLabel(trans('about.thanks'))
pygpt_net/ui/dialog/db.py CHANGED
@@ -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.09.05 18:00:00 #
9
+ # Updated Date: 2025.09.26 03:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from PySide6.QtCore import Qt, QTimer, QSignalBlocker
12
+ from PySide6.QtCore import Qt, QTimer, QSignalBlocker, QObject, QEvent
13
13
  from PySide6.QtGui import QAction, QIcon, QIntValidator
14
14
  from PySide6.QtWidgets import QGridLayout, QScrollArea, QSplitter, QComboBox, QLineEdit, QPushButton, \
15
- QHBoxLayout, QVBoxLayout, QLabel, QWidget, QSizePolicy, QCheckBox, QMenuBar, QAbstractItemView, QHeaderView, QStyledItemDelegate
15
+ QHBoxLayout, QVBoxLayout, QLabel, QWidget, QSizePolicy, QCheckBox, QMenuBar, QAbstractItemView, QHeaderView, QStyledItemDelegate, QMenu, QApplication
16
16
 
17
17
  from pygpt_net.ui.widget.dialog.db import DatabaseDialog
18
18
  from pygpt_net.ui.widget.lists.db import DatabaseList, DatabaseTableModel
@@ -32,6 +32,98 @@ class _FastTextDelegate(QStyledItemDelegate):
32
32
  return s
33
33
 
34
34
 
35
+ class _ContextMenuStyler(QObject):
36
+ """
37
+ Ensures that any QMenu spawned from the observed widgets adopts the same
38
+ application stylesheet, palette and font (e.g. Qt Material), even if
39
+ the menu is created without a parent or uses platform defaults.
40
+ The filter is installed only on specific widgets (no app-wide filters).
41
+ """
42
+
43
+ def __init__(self, style_source: QWidget):
44
+ super().__init__(style_source)
45
+ self._source = style_source
46
+ self._scan_timer = QTimer(self)
47
+ self._scan_timer.setInterval(50) # fast enough to catch freshly spawned menus
48
+ self._scan_timer.timeout.connect(self._scan_and_style)
49
+ self._scan_ticks = 0
50
+ self._scan_ticks_max = 8 # up to ~400ms window
51
+
52
+ def eventFilter(self, obj, event):
53
+ # Trigger styling round right before/around menu show moments
54
+ if event.type() in (QEvent.ContextMenu, QEvent.MouseButtonPress, QEvent.KeyPress):
55
+ if event.type() == QEvent.ContextMenu:
56
+ self._start_scan()
57
+ elif event.type() == QEvent.MouseButtonPress and hasattr(event, "button") and event.button() == Qt.RightButton:
58
+ self._start_scan()
59
+ elif event.type() == QEvent.KeyPress:
60
+ key = getattr(event, "key", lambda: None)()
61
+ mods = getattr(event, "modifiers", lambda: Qt.NoModifier)()
62
+ if key == Qt.Key_Menu or (key == Qt.Key_F10 and (mods & Qt.ShiftModifier)):
63
+ self._start_scan()
64
+ return False # do not consume events
65
+
66
+ def _start_scan(self):
67
+ # Start a short, repeated scan to reliably catch the menu top-level window
68
+ self._scan_ticks = 0
69
+ if not self._scan_timer.isActive():
70
+ # immediate pass + scheduled ones
71
+ QTimer.singleShot(0, self._scan_and_style)
72
+ self._scan_timer.start()
73
+
74
+ def _scan_and_style(self):
75
+ self._scan_ticks += 1
76
+ app = QApplication.instance()
77
+ if app is None:
78
+ self._scan_timer.stop()
79
+ return
80
+
81
+ found_any = False
82
+ for w in app.topLevelWidgets():
83
+ if isinstance(w, QMenu):
84
+ found_any = True
85
+ self._ensure_menu_style(w)
86
+
87
+ # Stop once we scanned enough frames, or if no menus appear after a short while
88
+ if self._scan_ticks >= self._scan_ticks_max or (self._scan_ticks >= 2 and not found_any):
89
+ self._scan_timer.stop()
90
+
91
+ def _ensure_menu_style(self, menu: QMenu):
92
+ # Avoid re-styling the same widget repeatedly
93
+ if bool(menu.property("_pygpt_menu_styled")):
94
+ return
95
+
96
+ app = QApplication.instance()
97
+ try:
98
+ # Apply application-level stylesheet (covers Qt Material CSS selectors for QMenu)
99
+ if app is not None:
100
+ ss = app.styleSheet()
101
+ if ss:
102
+ # Assign directly to the menu; do not append, to avoid duplication on repeated shows
103
+ menu.setStyleSheet(ss)
104
+
105
+ # Copy palette, font and style from the source window to guarantee readable contrast
106
+ src = self._source.window() if self._source is not None else None
107
+ if src is None and app is not None:
108
+ src = app.activeWindow()
109
+
110
+ if src is not None:
111
+ menu.setPalette(src.palette())
112
+ menu.setFont(src.font())
113
+ # Also align QStyle to prevent platform defaults from clashing with stylesheet
114
+ menu.setStyle(src.style())
115
+
116
+ # Mark as processed
117
+ menu.setProperty("_pygpt_menu_styled", True)
118
+
119
+ # Update to reflect changes immediately if the menu is already visible
120
+ if menu.isVisible():
121
+ menu.update()
122
+ except Exception:
123
+ # Fail-safe: never break the context menu on styling issues
124
+ pass
125
+
126
+
35
127
  class Database:
36
128
  def __init__(self, window=None):
37
129
  """
@@ -94,6 +186,9 @@ class Database:
94
186
  self.batch_actions_menu.addAction(self.delete_all_action)
95
187
  self.batch_actions_menu.addAction(self.truncate_action)
96
188
 
189
+ # Ensure QMenu used by menu bar follows the application/theme styling
190
+ self._apply_menu_style(self.batch_actions_menu)
191
+
97
192
  layout = QGridLayout()
98
193
  layout.addWidget(splitter, 1, 0)
99
194
  layout.setMenuBar(self.menu_bar)
@@ -102,6 +197,22 @@ class Database:
102
197
  self.window.ui.dialog['debug.db'].setLayout(layout)
103
198
  self.window.ui.dialog['debug.db'].setWindowTitle("Debug: Database (SQLite)")
104
199
 
200
+ def _apply_menu_style(self, menu: QMenu):
201
+ """Ensure QMenu follows application/theme styling consistently."""
202
+ try:
203
+ app = QApplication.instance()
204
+ if app is not None:
205
+ ss = app.styleSheet()
206
+ if ss:
207
+ menu.setStyleSheet(ss)
208
+ if self.window is not None:
209
+ menu.setStyle(self.window.style())
210
+ menu.setPalette(self.window.palette())
211
+ menu.setFont(self.window.font())
212
+ except Exception:
213
+ # Never break UI if styling fails
214
+ pass
215
+
105
216
  def _on_splitter_moved(self, pos, index):
106
217
  if self._text_viewer is not None:
107
218
  self._text_viewer.setUpdatesEnabled(False)
@@ -267,6 +378,34 @@ class DataBrowser(QWidget):
267
378
  self.setLayout(main_layout)
268
379
 
269
380
  view = self.get_list_widget()
381
+
382
+ # Robust, local-only context menu styling (no app-wide filters)
383
+ self._menu_styler = _ContextMenuStyler(self.window if self.window is not None else self)
384
+ if hasattr(view, "installEventFilter"):
385
+ view.installEventFilter(self._menu_styler)
386
+ # Context menu usually originates from the viewport in item views
387
+ if hasattr(view, "viewport") and callable(view.viewport):
388
+ vp = view.viewport()
389
+ if vp is not None and hasattr(vp, "installEventFilter"):
390
+ vp.installEventFilter(self._menu_styler)
391
+ # Also watch headers (they may expose their own context menus)
392
+ if hasattr(view, "horizontalHeader"):
393
+ hh = view.horizontalHeader()
394
+ if hh is not None:
395
+ hh.installEventFilter(self._menu_styler)
396
+ if hasattr(hh, "viewport"):
397
+ hvp = hh.viewport()
398
+ if hvp is not None:
399
+ hvp.installEventFilter(self._menu_styler)
400
+ if hasattr(view, "verticalHeader"):
401
+ vh = view.verticalHeader()
402
+ if vh is not None:
403
+ vh.installEventFilter(self._menu_styler)
404
+ if hasattr(vh, "viewport"):
405
+ vvp = vh.viewport()
406
+ if vvp is not None:
407
+ vvp.installEventFilter(self._menu_styler)
408
+
270
409
  if hasattr(view, "setUniformRowHeights"):
271
410
  view.setUniformRowHeights(True)
272
411
  if hasattr(view, "setWordWrap"):
@@ -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.28 09:00:00 #
9
+ # Updated Date: 2025.09.25 13:04:51 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import Qt
@@ -156,6 +156,8 @@ class Preset(BaseConfigDialog):
156
156
  modes = QWidget()
157
157
  modes.setLayout(rows_mode)
158
158
  modes.setContentsMargins(0, 0, 0, 0)
159
+ # Ensure modes column never grows taller than its content; avoids creating vertical gap above the splitter.
160
+ modes.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
159
161
  self.window.ui.nodes['preset.editor.modes'] = modes
160
162
 
161
163
  # experts
@@ -208,7 +210,7 @@ class Preset(BaseConfigDialog):
208
210
  # special case with settings icon on right
209
211
  node_layout = QHBoxLayout()
210
212
  builder_btn = QPushButton(QIcon(":/icons/robot.svg"), "")
211
- builder_btn.setToolTip("Open Agents Builder (beta)")
213
+ builder_btn.setToolTip(trans("agent.builder.tooltip"))
212
214
  builder_btn.clicked.connect(lambda: self.window.tools.get("agent_builder").toggle())
213
215
  node_layout.setContentsMargins(0, 0, 0, 0)
214
216
  node_layout.addLayout(options[key])
@@ -265,6 +267,8 @@ class Preset(BaseConfigDialog):
265
267
  widget_base = QWidget()
266
268
  widget_base.setLayout(rows)
267
269
  widget_base.setMinimumWidth(300)
270
+ # Keep base column at content height; do not stretch vertically.
271
+ widget_base.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
268
272
 
269
273
  self.window.ui.nodes['preset.editor.experts'].setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
270
274
 
@@ -274,12 +278,16 @@ class Preset(BaseConfigDialog):
274
278
 
275
279
  widget_main = QWidget()
276
280
  widget_main.setLayout(main)
281
+ # Critical: ensure the whole top pane (base + modes) stays at content height.
282
+ widget_main.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum)
277
283
 
278
284
  splitter = QSplitter(Qt.Vertical)
279
285
  splitter.addWidget(widget_main)
280
286
  splitter.addWidget(widget_prompt)
281
- splitter.setStretchFactor(0, 1)
282
- splitter.setStretchFactor(1, 2)
287
+ splitter.setChildrenCollapsible(False) # avoid accidental collapsing of any pane
288
+ # All extra vertical space goes to bottom (extra agent options); top stays at its sizeHint.
289
+ splitter.setStretchFactor(0, 0)
290
+ splitter.setStretchFactor(1, 1)
283
291
  self.window.ui.splitters['editor.presets'] = splitter
284
292
 
285
293
  widget_personalize = QWidget()
@@ -398,13 +406,17 @@ class AvatarWidget(QWidget):
398
406
  main_layout.setContentsMargins(0, 0, 0, 0)
399
407
 
400
408
  current_avatar_label = QLabel(trans("preset.personalize.avatar.current"))
409
+ # Center the section title for better visual balance.
410
+ current_avatar_label.setAlignment(Qt.AlignHCenter)
401
411
  main_layout.addWidget(current_avatar_label)
402
412
 
403
413
  self.avatar_preview = QLabel(self)
404
414
  self.avatar_preview.setFixedSize(200, 200)
405
- self.avatar_preview.setStyleSheet("border: 1px solid gray;")
415
+ # Keep a visible border, but round it to visually indicate the circular avatar.
416
+ self.avatar_preview.setStyleSheet("border: 1px solid gray; border-radius: 100px; background: transparent;")
406
417
  self.avatar_preview.setAlignment(Qt.AlignCenter)
407
- main_layout.addWidget(self.avatar_preview)
418
+ # Center the avatar widget within the column.
419
+ main_layout.addWidget(self.avatar_preview, 0, Qt.AlignHCenter)
408
420
 
409
421
  buttons_layout = QHBoxLayout()
410
422
 
@@ -417,6 +429,8 @@ class AvatarWidget(QWidget):
417
429
  self.remove_button.setEnabled(False)
418
430
  buttons_layout.addWidget(self.remove_button)
419
431
  buttons_layout.setContentsMargins(0, 10, 0, 0)
432
+ # Center the action buttons below the avatar.
433
+ buttons_layout.setAlignment(Qt.AlignCenter)
420
434
 
421
435
  main_layout.addLayout(buttons_layout)
422
436
  main_layout.setContentsMargins(0, 10, 0, 0)
@@ -440,7 +454,9 @@ class AvatarWidget(QWidget):
440
454
  pixmap = QPixmap(file_path)
441
455
  if not pixmap.isNull():
442
456
  cover_pix = self.get_cover_pixmap(pixmap, self.avatar_preview.width(), self.avatar_preview.height())
443
- self.avatar_preview.setPixmap(cover_pix)
457
+ # Render the avatar as a circular pixmap.
458
+ circle_pix = self.get_circular_pixmap(cover_pix, self.avatar_preview.width(), self.avatar_preview.height())
459
+ self.avatar_preview.setPixmap(circle_pix)
444
460
  self.remove_button.setEnabled(True)
445
461
 
446
462
  def enable_remove_button(self, enabled: bool = True):
@@ -472,7 +488,30 @@ class AvatarWidget(QWidget):
472
488
  y = (scaled_pix.height() - target_height) // 2
473
489
  return scaled_pix.copy(x, y, target_width, target_height)
474
490
 
491
+ def get_circular_pixmap(self, pixmap, target_width: int, target_height: int):
492
+ """
493
+ Create a circular version of the given pixmap using an antialiased clip path.
494
+
495
+ :param pixmap: Source pixmap already scaled/cropped to target size
496
+ :param target_width: Target width
497
+ :param target_height: Target height
498
+ :return: Circular masked pixmap with transparent corners
499
+ """
500
+ from PySide6.QtGui import QPainter, QPainterPath, QPixmap # local import to avoid altering global imports
501
+ result = QPixmap(target_width, target_height)
502
+ result.fill(Qt.transparent)
503
+
504
+ painter = QPainter(result)
505
+ painter.setRenderHint(QPainter.Antialiasing, True)
506
+ path = QPainterPath()
507
+ path.addEllipse(0, 0, target_width, target_height)
508
+ painter.setClipPath(path)
509
+ painter.drawPixmap(0, 0, pixmap)
510
+ painter.end()
511
+
512
+ return result
513
+
475
514
  def remove_avatar(self):
476
515
  """Remove the current avatar image."""
477
516
  self.avatar_preview.clear()
478
- self.remove_button.setEnabled(False)
517
+ self.remove_button.setEnabled(False)