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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. pygpt_net/CHANGELOG.txt +12 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/chat/handler/anthropic_stream.py +0 -2
  4. pygpt_net/controller/chat/handler/worker.py +6 -2
  5. pygpt_net/controller/debug/debug.py +6 -6
  6. pygpt_net/controller/model/editor.py +20 -42
  7. pygpt_net/controller/model/importer.py +9 -2
  8. pygpt_net/controller/painter/common.py +0 -8
  9. pygpt_net/controller/plugins/plugins.py +11 -3
  10. pygpt_net/controller/presets/presets.py +2 -2
  11. pygpt_net/core/ctx/bag.py +7 -2
  12. pygpt_net/core/ctx/reply.py +17 -2
  13. pygpt_net/core/db/viewer.py +19 -34
  14. pygpt_net/core/render/plain/pid.py +12 -1
  15. pygpt_net/core/render/web/body.py +292 -445
  16. pygpt_net/core/tabs/tab.py +24 -1
  17. pygpt_net/data/config/config.json +3 -3
  18. pygpt_net/data/config/models.json +3 -3
  19. pygpt_net/item/assistant.py +51 -2
  20. pygpt_net/item/attachment.py +21 -20
  21. pygpt_net/item/calendar_note.py +19 -2
  22. pygpt_net/item/ctx.py +115 -2
  23. pygpt_net/item/index.py +9 -2
  24. pygpt_net/item/mode.py +9 -6
  25. pygpt_net/item/model.py +20 -3
  26. pygpt_net/item/notepad.py +14 -2
  27. pygpt_net/item/preset.py +42 -2
  28. pygpt_net/item/prompt.py +8 -2
  29. pygpt_net/plugin/cmd_files/plugin.py +2 -2
  30. pygpt_net/provider/api/anthropic/tools.py +1 -1
  31. pygpt_net/provider/api/google/realtime/client.py +2 -2
  32. pygpt_net/provider/core/attachment/json_file.py +2 -2
  33. pygpt_net/tools/text_editor/tool.py +4 -1
  34. pygpt_net/tools/text_editor/ui/dialogs.py +1 -1
  35. pygpt_net/ui/dialog/db.py +177 -59
  36. pygpt_net/ui/dialog/dictionary.py +57 -59
  37. pygpt_net/ui/dialog/editor.py +3 -2
  38. pygpt_net/ui/dialog/image.py +1 -1
  39. pygpt_net/ui/dialog/logger.py +3 -2
  40. pygpt_net/ui/dialog/models.py +171 -21
  41. pygpt_net/ui/dialog/plugins.py +26 -20
  42. pygpt_net/ui/layout/ctx/ctx_list.py +3 -4
  43. pygpt_net/ui/layout/toolbox/__init__.py +2 -2
  44. pygpt_net/ui/layout/toolbox/assistants.py +8 -9
  45. pygpt_net/ui/layout/toolbox/presets.py +2 -2
  46. pygpt_net/ui/main.py +9 -4
  47. pygpt_net/ui/widget/element/labels.py +2 -2
  48. pygpt_net/ui/widget/textarea/editor.py +0 -4
  49. pygpt_net/utils.py +12 -13
  50. {pygpt_net-2.6.37.dist-info → pygpt_net-2.6.39.dist-info}/METADATA +14 -2
  51. {pygpt_net-2.6.37.dist-info → pygpt_net-2.6.39.dist-info}/RECORD +54 -54
  52. {pygpt_net-2.6.37.dist-info → pygpt_net-2.6.39.dist-info}/LICENSE +0 -0
  53. {pygpt_net-2.6.37.dist-info → pygpt_net-2.6.39.dist-info}/WHEEL +0 -0
  54. {pygpt_net-2.6.37.dist-info → pygpt_net-2.6.39.dist-info}/entry_points.txt +0 -0
@@ -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.11 18:00:00 #
9
+ # Updated Date: 2025.09.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtWidgets import QPushButton, QHBoxLayout, QVBoxLayout
@@ -44,7 +44,8 @@ class Logger:
44
44
 
45
45
  self.window.ui.nodes['logger.btn.clear'] = QPushButton(trans("dialog.logger.btn.clear"))
46
46
  self.window.ui.nodes['logger.btn.clear'].clicked.connect(
47
- lambda: self.window.controller.debug.clear_logger())
47
+ lambda: self.window.controller.debug.clear_logger()
48
+ )
48
49
 
49
50
  bottom_layout = QHBoxLayout()
50
51
  bottom_layout.addWidget(self.window.ui.nodes['logger.btn.clear'])
@@ -6,17 +6,17 @@
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.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
13
+ from typing import Dict, List, Optional
13
14
 
14
15
  from PySide6.QtCore import Qt
15
16
  from PySide6.QtGui import QStandardItemModel, QIcon
16
17
  from PySide6.QtWidgets import QPushButton, QHBoxLayout, QLabel, QVBoxLayout, QScrollArea, QWidget, QTabWidget, QFrame, \
17
18
  QSplitter, QSizePolicy
18
19
 
19
- from pygpt_net.item.model import ModelItem
20
20
  from pygpt_net.ui.widget.dialog.model import ModelDialog
21
21
  from pygpt_net.ui.widget.element.group import CollapsedGroup
22
22
  from pygpt_net.ui.widget.element.labels import UrlLabel, DescLabel
@@ -28,8 +28,10 @@ from pygpt_net.ui.widget.option.dictionary import OptionDict
28
28
  from pygpt_net.ui.widget.option.input import OptionInput, PasswordInput
29
29
  from pygpt_net.ui.widget.option.slider import OptionSlider
30
30
  from pygpt_net.ui.widget.option.textarea import OptionTextarea
31
+ from pygpt_net.ui.widget.textarea.search_input import SearchInput
31
32
  from pygpt_net.utils import trans
32
33
 
34
+
33
35
  class Models:
34
36
  def __init__(self, window=None):
35
37
  """
@@ -40,6 +42,13 @@ class Models:
40
42
  self.window = window
41
43
  self.dialog_id = "models.editor"
42
44
 
45
+ # Internal state for filtering/mapping
46
+ self._filter_text: str = ""
47
+ self._filtered_ids: List[str] = [] # current list of model keys displayed in the view (order matters)
48
+ self._index_to_id: List[str] = [] # row index -> model key
49
+ self._id_to_index: Dict[str, int] = {} # model key -> row index
50
+ self._all_data: Dict[str, object] = {} # last known (unfiltered) data snapshot used to render the list
51
+
43
52
  def setup(self, idx=None):
44
53
  """
45
54
  Setup editor dialog
@@ -56,13 +65,17 @@ class Models:
56
65
  QPushButton(trans("dialog.models.editor.btn.save"))
57
66
 
58
67
  self.window.ui.nodes['models.editor.btn.new'].clicked.connect(
59
- lambda: self.window.controller.model.editor.new())
68
+ lambda: self.window.controller.model.editor.new()
69
+ )
60
70
  self.window.ui.nodes['models.editor.btn.defaults.user'].clicked.connect(
61
- lambda: self.window.controller.model.editor.load_defaults_user())
71
+ lambda: self.window.controller.model.editor.load_defaults_user()
72
+ )
62
73
  self.window.ui.nodes['models.editor.btn.defaults.app'].clicked.connect(
63
- lambda: self.window.controller.model.editor.load_defaults_app())
74
+ lambda: self.window.controller.model.editor.load_defaults_app()
75
+ )
64
76
  self.window.ui.nodes['models.editor.btn.save'].clicked.connect(
65
- lambda: self.window.controller.model.editor.save())
77
+ lambda: self.window.controller.model.editor.save()
78
+ )
66
79
 
67
80
  # set enter key to save button
68
81
  self.window.ui.nodes['models.editor.btn.new'].setAutoDefault(False)
@@ -111,17 +124,16 @@ class Models:
111
124
  # append advanced options at the end
112
125
  if len(advanced_keys) > 0:
113
126
  group_id = 'models.editor.advanced'
114
- self.window.ui.groups[group_id] = CollapsedGroup(self.window, group_id, None, False, None)
115
- self.window.ui.groups[group_id].box.setText(trans('settings.advanced.collapse'))
127
+ group = CollapsedGroup(self.window, group_id, None, False, None)
128
+ group.box.setText(trans('settings.advanced.collapse'))
116
129
  for key in widgets:
117
130
  if key not in advanced_keys: # ignore non-advanced options
118
131
  continue
119
-
120
- option = self.add_option(widgets[key], options[key]) # build option
121
- self.window.ui.groups[group_id].add_layout(option) # add option to group
132
+ group.add_layout(self.add_option(widgets[key], options[key])) # add option to group
122
133
 
123
134
  # add advanced options group to scroll
124
- content.addWidget(self.window.ui.groups[group_id])
135
+ content.addWidget(group)
136
+ self.window.ui.groups[group_id] = group
125
137
 
126
138
  content.addStretch()
127
139
 
@@ -146,16 +158,29 @@ class Models:
146
158
  self.window.ui.nodes[id] = ModelEditorList(self.window, id)
147
159
  self.window.ui.models[id] = self.create_model(self.window)
148
160
  self.window.ui.nodes[id].setModel(self.window.ui.models[id])
149
-
150
- # update models list
161
+ self.window.ui.nodes[id].setMinimumWidth(250) # set max width to list
162
+
163
+ # search input (placed above the list)
164
+ self.window.ui.nodes['models.editor.search'] = SearchInput()
165
+ # Connect to provided callback API (callables assigned to attributes)
166
+ self.window.ui.nodes['models.editor.search'].on_search = self._on_search_models
167
+ self.window.ui.nodes['models.editor.search'].on_clear = self._on_clear_models # clear via "X" button
168
+
169
+ # container for search + list (left panel)
170
+ left_layout = QVBoxLayout()
171
+ left_layout.setContentsMargins(0, 0, 0, 0)
172
+ left_layout.setSpacing(6)
173
+ left_layout.addWidget(self.window.ui.nodes['models.editor.search'])
174
+ left_layout.addWidget(self.window.ui.nodes[id])
175
+ left_widget = QWidget()
176
+ left_widget.setLayout(left_layout)
177
+
178
+ # update models list (initial, unfiltered)
151
179
  self.update_list(id, data)
152
180
 
153
- # set max width to list
154
- self.window.ui.nodes[id].setMinimumWidth(250)
155
-
156
181
  # splitter
157
182
  self.window.ui.splitters['dialog.models'] = QSplitter(Qt.Horizontal)
158
- self.window.ui.splitters['dialog.models'].addWidget(self.window.ui.nodes[id]) # list
183
+ self.window.ui.splitters['dialog.models'].addWidget(left_widget) # search + list
159
184
  self.window.ui.splitters['dialog.models'].addWidget(area_widget) # tabs
160
185
  self.window.ui.splitters['dialog.models'].setStretchFactor(0, 2)
161
186
  self.window.ui.splitters['dialog.models'].setStretchFactor(1, 5)
@@ -182,6 +207,86 @@ class Models:
182
207
  if self.window.controller.model.editor.current is None:
183
208
  self.window.controller.model.editor.set_by_tab(0)
184
209
 
210
+ def _on_search_models(self, text: str):
211
+ """
212
+ Handle SearchInput callback. Filtering is prefix-based on model id or name (case-insensitive).
213
+ """
214
+ # Keep normalized filter text
215
+ self._filter_text = (text or "").strip().casefold()
216
+ # Re-apply list render using the last known dataset
217
+ self.update_list('models.list', self._all_data)
218
+ # Do not force-select rows here to avoid heavy editor re-initialization on each keystroke.
219
+
220
+ def _on_clear_models(self):
221
+ """
222
+ Handle SearchInput clear (click on 'X' button).
223
+ """
224
+ if not self._filter_text:
225
+ return
226
+ self._filter_text = ""
227
+ self.update_list('models.list', self._all_data)
228
+ # Try to restore selection for the current model if it is visible again.
229
+ self._restore_selection_for_current()
230
+
231
+ def _restore_selection_for_current(self):
232
+ """
233
+ Attempt to restore selection on the list to the current editor model if present in the view.
234
+ """
235
+ current_id = getattr(self.window.controller.model.editor, "current", None)
236
+ if not current_id:
237
+ return
238
+ idx = self.get_row_by_model_id(current_id)
239
+ if idx is None:
240
+ return
241
+ current = self.window.ui.models['models.list'].index(idx, 0)
242
+ self.window.ui.nodes['models.list'].setCurrentIndex(current)
243
+
244
+ def _apply_filter(self, data: Dict[str, object]) -> Dict[str, object]:
245
+ """
246
+ Return filtered data view, preserving original order.
247
+ """
248
+ if not self._filter_text:
249
+ return data
250
+
251
+ out: Dict[str, object] = {}
252
+ needle = self._filter_text
253
+ for mid, item in data.items():
254
+ # Normalize fields for prefix matching
255
+ model_id = (str(getattr(item, "id", mid)) or "").casefold()
256
+ model_name = (getattr(item, "name", "") or "").casefold()
257
+ if (needle in model_name
258
+ or needle in model_id):
259
+ out[mid] = item
260
+ return out
261
+
262
+ def _refresh_index_mapping(self, ids: List[str]):
263
+ """
264
+ Build fast row<->id mapping for the current filtered list.
265
+ """
266
+ self._filtered_ids = list(ids)
267
+ self._index_to_id = list(ids)
268
+ self._id_to_index = {mid: idx for idx, mid in enumerate(ids)}
269
+
270
+ def get_model_id_by_row(self, row: int) -> Optional[str]:
271
+ """
272
+ Map a view row index to the model id currently displayed at that row.
273
+ """
274
+ if 0 <= row < len(self._index_to_id):
275
+ return self._index_to_id[row]
276
+ return None
277
+
278
+ def get_row_by_model_id(self, model_id: str) -> Optional[int]:
279
+ """
280
+ Map a model id to its current row index in the filtered view.
281
+ """
282
+ return self._id_to_index.get(model_id)
283
+
284
+ def get_filtered_ids(self) -> List[str]:
285
+ """
286
+ Return a copy of currently visible model ids (filtered order).
287
+ """
288
+ return list(self._filtered_ids)
289
+
185
290
  def build_widgets(self, options: dict) -> dict:
186
291
  """
187
292
  Build settings options widgets
@@ -331,6 +436,21 @@ class Models:
331
436
  """
332
437
  return QStandardItemModel(0, 1, parent)
333
438
 
439
+ def _get_provider_display_name(self, provider_id: str) -> str:
440
+ """
441
+ Resolve provider display name using app's LLM provider registry.
442
+ """
443
+ if not provider_id:
444
+ return ""
445
+ try:
446
+ name = self.window.core.llm.get_provider_name(provider_id)
447
+ if isinstance(name, str):
448
+ name = name.strip()
449
+ return name or ""
450
+ except Exception:
451
+ # Fail silently to avoid breaking the models list rendering
452
+ return ""
453
+
334
454
  def update_list(self, id: str, data: dict):
335
455
  """
336
456
  Update list
@@ -338,10 +458,40 @@ class Models:
338
458
  :param id: ID of the list
339
459
  :param data: Data to update
340
460
  """
461
+ # Keep latest source data for subsequent filtering cycles
462
+ self._all_data = dict(data)
463
+
464
+ # Apply current filter (preserve insertion/sorted order)
465
+ filtered = self._apply_filter(self._all_data)
466
+
467
+ # Reset model rows
341
468
  self.window.ui.models[id].removeRows(0, self.window.ui.models[id].rowCount())
469
+
470
+ # Count occurrences of each model display name to detect duplicates (in filtered view)
471
+ name_counts = {}
472
+ for key in filtered:
473
+ item = filtered[key]
474
+ base_name = getattr(item, "name", "") or ""
475
+ name_counts[base_name] = name_counts.get(base_name, 0) + 1
476
+
477
+ # Populate rows with optional provider suffix for duplicate names
342
478
  i = 0
343
- for n in data:
479
+ row_ids: List[str] = []
480
+ for n in filtered:
481
+ item = filtered[n]
482
+ base_name = getattr(item, "name", "") or ""
483
+ display_name = base_name
484
+ if name_counts.get(base_name, 0) > 1:
485
+ provider_id = getattr(item, "provider", None)
486
+ provider_name = self._get_provider_display_name(provider_id) if provider_id else ""
487
+ if provider_name:
488
+ display_name = f"{base_name} ({provider_name})"
489
+
344
490
  self.window.ui.models[id].insertRow(i)
345
- name = data[n].name
346
- self.window.ui.models[id].setData(self.window.ui.models[id].index(i, 0), name)
491
+ self.window.ui.models[id].setData(self.window.ui.models[id].index(i, 0), display_name)
492
+ # IMPORTANT: map list row to the dictionary key (stable and unique within models.items)
493
+ row_ids.append(n)
347
494
  i += 1
495
+
496
+ # Refresh index mappings used by Editor
497
+ self._refresh_index_mapping(row_ids)
@@ -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.04 00:00:00 #
9
+ # Updated Date: 2025.09.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import Qt
@@ -55,11 +55,14 @@ class Plugins:
55
55
  QPushButton(trans("dialog.plugin.settings.btn.save"))
56
56
 
57
57
  self.window.ui.nodes['plugin.settings.btn.defaults.user'].clicked.connect(
58
- lambda: self.window.controller.plugins.settings.load_defaults_user())
58
+ lambda: self.window.controller.plugins.settings.load_defaults_user()
59
+ )
59
60
  self.window.ui.nodes['plugin.settings.btn.defaults.app'].clicked.connect(
60
- lambda: self.window.controller.plugins.settings.load_defaults_app())
61
+ lambda: self.window.controller.plugins.settings.load_defaults_app()
62
+ )
61
63
  self.window.ui.nodes['plugin.settings.btn.save'].clicked.connect(
62
- lambda: self.window.controller.plugins.settings.save())
64
+ lambda: self.window.controller.plugins.settings.save()
65
+ )
63
66
 
64
67
  # set enter key to save button
65
68
  self.window.ui.nodes['plugin.settings.btn.defaults.user'].setAutoDefault(False)
@@ -321,33 +324,35 @@ class Plugins:
321
324
  widgets = {}
322
325
  base_types = ('text', 'int', 'float')
323
326
  number_types = ('int', 'float')
327
+ apply = self.window.controller.config.placeholder.apply
324
328
 
325
329
  for key in options:
326
330
  option = options[key]
331
+ t = option['type']
327
332
  # create widget by option type
328
- if option['type'] in base_types:
329
- if 'slider' in option and option['slider'] and option['type'] in number_types:
333
+ if t in base_types:
334
+ if 'slider' in option and option['slider'] and t in number_types:
330
335
  widgets[key] = OptionSlider(self.window, parent, key, option) # slider + text input
331
336
  else:
332
337
  if 'secret' in option and option['secret']:
333
338
  widgets[key] = PasswordInput(self.window, parent, key, option) # password input
334
339
  else:
335
340
  widgets[key] = OptionInput(self.window, parent, key, option) # text input
336
- elif option['type'] == 'textarea':
341
+ elif t == 'textarea':
337
342
  widgets[key] = OptionTextarea(self.window, parent, key, option) # textarea
338
- elif option['type'] == 'bool':
343
+ elif t == 'bool':
339
344
  widgets[key] = OptionCheckbox(self.window, parent, key, option) # checkbox
340
- elif option['type'] == 'bool_list':
341
- self.window.controller.config.placeholder.apply(option)
345
+ elif t == 'bool_list':
346
+ apply(option)
342
347
  widgets[key] = OptionCheckboxList(self.window, parent, key, option) # checkbox list
343
- elif option['type'] == 'dict':
344
- self.window.controller.config.placeholder.apply(option)
348
+ elif t == 'dict':
349
+ apply(option)
345
350
  widgets[key] = OptionDict(self.window, parent, key, option) # dictionary
346
- elif option['type'] == 'combo':
347
- self.window.controller.config.placeholder.apply(option)
351
+ elif t == 'combo':
352
+ apply(option)
348
353
  widgets[key] = OptionCombo(self.window, parent, key, option) # combobox
349
- elif option['type'] == 'cmd':
350
- self.window.controller.config.placeholder.apply(option)
354
+ elif t == 'cmd':
355
+ apply(option)
351
356
  widgets[key] = OptionCmd(self.window, plugin, parent, key, option) # command
352
357
 
353
358
  return widgets
@@ -508,12 +513,13 @@ class Plugins:
508
513
  :param id: ID of the list
509
514
  :param data: Data to update
510
515
  """
511
- self.window.ui.models[id].removeRows(0, self.window.ui.models[id].rowCount())
516
+ model = self.window.ui.models[id]
517
+ model.removeRows(0, model.rowCount())
512
518
  i = 0
513
519
  for n in data:
514
- self.window.ui.models[id].insertRow(i)
520
+ model.insertRow(i)
515
521
  name = self.window.core.plugins.get_name(data[n].id)
516
522
  tooltip = self.window.core.plugins.get_desc(data[n].id)
517
- self.window.ui.models[id].setData(self.window.ui.models[id].index(i, 0), name)
518
- self.window.ui.models[id].setData(self.window.ui.models[id].index(i, 0), tooltip, Qt.ToolTipRole)
523
+ model.setData(model.index(i, 0), name)
524
+ model.setData(model.index(i, 0), tooltip, Qt.ToolTipRole)
519
525
  i += 1
@@ -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.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6 import QtCore
@@ -210,7 +210,7 @@ class CtxList:
210
210
  files_str = ", ".join(files)
211
211
  if len(files_str) > 40:
212
212
  files_str = files_str[:40] + '...'
213
- tooltip_str = trans("attachments.ctx.tooltip.list").format(num=len(files)) + ": " + files_str
213
+ tooltip_str = f"{trans('attachments.ctx.tooltip.list').format(num=len(files))}: {files_str}"
214
214
  group_item.setToolTip(tooltip_str)
215
215
 
216
216
  group_item.setData(custom_data, QtCore.Qt.ItemDataRole.UserRole)
@@ -282,8 +282,7 @@ class CtxList:
282
282
  files_str = ", ".join(files)
283
283
  if len(files_str) > 40:
284
284
  files_str = files_str[:40] + '...'
285
- tooltip_str = trans("attachments.ctx.tooltip.list").format(num=len(files)) + ": " + files_str
286
- tooltip_text += "\n" + tooltip_str
285
+ tooltip_text += f"\n{trans('attachments.ctx.tooltip.list').format(num=len(files))}: {files_str}"
287
286
 
288
287
  item = Item(name, id)
289
288
  item.id = id
@@ -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: 2024.11.17 03:00:00 #
9
+ # Updated Date: 2025.09.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from .toolbox import *
12
+ from .toolbox import ToolboxMain
@@ -115,15 +115,14 @@ class Assistants:
115
115
  view.backup_selection()
116
116
  view.setUpdatesEnabled(False)
117
117
  try:
118
- with QtCore.QSignalBlocker(model):
119
- model.setRowCount(0)
120
- count = len(data)
121
- if count:
122
- model.setRowCount(count)
123
- for i, item in enumerate(data.values()):
124
- index = model.index(i, 0)
125
- model.setData(index, "ID: " + item.id, QtCore.Qt.ToolTipRole)
126
- model.setData(index, item.name)
118
+ model.setRowCount(0)
119
+ count = len(data)
120
+ if count:
121
+ model.setRowCount(count)
122
+ for i, item in enumerate(data.values()):
123
+ index = model.index(i, 0)
124
+ model.setData(index, "ID: " + item.id, QtCore.Qt.ToolTipRole)
125
+ model.setData(index, item.name)
127
126
  finally:
128
127
  view.setUpdatesEnabled(True)
129
128
 
@@ -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.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6 import QtCore
@@ -144,7 +144,7 @@ class Presets:
144
144
  for i, (key, item) in enumerate(data.items()):
145
145
  name = item.name
146
146
  if is_expert_mode and item.enabled and not key.startswith(startswith_current):
147
- name = "[x] " + name
147
+ name = f"[x] {name}"
148
148
  elif is_agent_mode:
149
149
  num_experts = count_experts(key)
150
150
  if num_experts > 0:
pygpt_net/ui/main.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.08.27 07:00:00 #
9
+ # Updated Date: 2025.09.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -231,7 +231,7 @@ class MainWindow(QMainWindow, QtStyleTools):
231
231
 
232
232
  def update(self):
233
233
  """Called on every update (real-time)"""
234
- self.controller.on_update()
234
+ # self.controller.on_update()
235
235
  self.controller.plugins.on_update()
236
236
  self.tools.on_update()
237
237
 
@@ -254,8 +254,13 @@ class MainWindow(QMainWindow, QtStyleTools):
254
254
 
255
255
  :param message: status message
256
256
  """
257
- message = message if isinstance(message, str) else str(message)
258
- self.dispatch(KernelEvent(KernelEvent.STATUS, {"status": message}))
257
+ self.dispatch(
258
+ KernelEvent(
259
+ KernelEvent.STATUS, {
260
+ "status": message if isinstance(message, str) else str(message)
261
+ }
262
+ )
263
+ )
259
264
 
260
265
  @Slot(str)
261
266
  def update_state(self, state: str):
@@ -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.04 00:00:00 #
9
+ # Updated Date: 2025.09.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import Qt, QTimer, QRect, Signal, QUrl, QEvent
@@ -50,7 +50,7 @@ class ChatStatusLabel(QLabel):
50
50
  def __init__(self, text, window=None):
51
51
  super().__init__(text, window)
52
52
  self.window = window
53
- self.setWordWrap(True)
53
+ self.setWordWrap(False)
54
54
  self.setAlignment(Qt.AlignRight)
55
55
  self.setProperty('class', 'label-chat-status')
56
56
  self.setTextInteractionFlags(Qt.TextSelectableByMouse)
@@ -113,10 +113,6 @@ class BaseCodeEditor(QTextEdit):
113
113
  self.finder.clear()
114
114
 
115
115
  def on_destroy(self):
116
- try:
117
- self.textChanged.disconnect(self.text_changed)
118
- except Exception:
119
- pass
120
116
  self.window.controller.finder.unset(self.finder)
121
117
 
122
118
  def keyPressEvent(self, e):
pygpt_net/utils.py CHANGED
@@ -269,28 +269,25 @@ def natsort(l: list) -> list:
269
269
  return sorted(l, key=alphanum_key)
270
270
 
271
271
  def mem_clean():
272
- """
273
- Clean memory by removing unused variables
274
- """
275
- return # temporary disabled
276
-
272
+ """Clean memory by removing unused objects"""
273
+ return
277
274
  import sys, gc
278
275
  ok = False
279
276
  try:
280
277
  gc.collect()
281
- except Exception:
282
- pass
278
+ except Exception as e:
279
+ print(e)
283
280
 
284
281
  try:
285
282
  QApplication.sendPostedEvents(None, QtCore.QEvent.DeferredDelete)
286
283
  QApplication.processEvents(QtCore.QEventLoop.AllEvents, 50)
287
- except Exception:
288
- pass
284
+ except Exception as e:
285
+ print(e)
289
286
 
290
287
  try:
291
288
  QtGui.QPixmapCache.clear()
292
- except Exception:
293
- pass
289
+ except Exception as e:
290
+ print(e)
294
291
 
295
292
  try:
296
293
  if sys.platform.startswith("linux"):
@@ -299,6 +296,7 @@ def mem_clean():
299
296
  if hasattr(libc, "malloc_trim"):
300
297
  libc.malloc_trim(0)
301
298
  ok = True
299
+ '''
302
300
  elif sys.platform == "win32":
303
301
  import ctypes, ctypes.wintypes
304
302
  kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
@@ -322,6 +320,7 @@ def mem_clean():
322
320
  ok = True
323
321
  except Exception:
324
322
  pass
325
- except Exception:
326
- pass
323
+ '''
324
+ except Exception as e:
325
+ print(e)
327
326
  return ok
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pygpt-net
3
- Version: 2.6.37
3
+ Version: 2.6.39
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, 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
@@ -118,7 +118,7 @@ Description-Content-Type: text/markdown
118
118
 
119
119
  [![pygpt](https://snapcraft.io/pygpt/badge.svg)](https://snapcraft.io/pygpt)
120
120
 
121
- Release: **2.6.37** | build: **2025-09-05** | Python: **>=3.10, <3.14**
121
+ Release: **2.6.39** | build: **2025-09-06** | Python: **>=3.10, <3.14**
122
122
 
123
123
  > Official website: https://pygpt.net | Documentation: https://pygpt.readthedocs.io
124
124
  >
@@ -3565,6 +3565,18 @@ may consume additional tokens that are not displayed in the main window.
3565
3565
 
3566
3566
  ## Recent changes:
3567
3567
 
3568
+ **2.6.39 (2025-09-06)**
3569
+
3570
+ - Added: Search input to the models editor.
3571
+ - Fixed: Brush color switching when changing modes in the Painter.
3572
+
3573
+ **2.6.38 (2025-09-05)**
3574
+
3575
+ - Fixed: Detection of chunk type in Ollama.
3576
+ - Fixed: Import of models with existing IDs.
3577
+ - Fixed: Updating the Assistants UI list after creating a new Assistant.
3578
+ - Refactored and optimized the code.
3579
+
3568
3580
  **2.6.37 (2025-09-05)**
3569
3581
 
3570
3582
  - Fixed: Function parameters sanitization in the Google Gen AI SDK.