pygpt-net 2.6.67__py3-none-any.whl → 2.7.1__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 (78) hide show
  1. pygpt_net/CHANGELOG.txt +20 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/assistant/assistant.py +13 -8
  4. pygpt_net/controller/assistant/batch.py +29 -15
  5. pygpt_net/controller/assistant/files.py +19 -14
  6. pygpt_net/controller/assistant/store.py +63 -41
  7. pygpt_net/controller/attachment/attachment.py +45 -35
  8. pygpt_net/controller/chat/attachment.py +50 -39
  9. pygpt_net/controller/config/field/dictionary.py +26 -14
  10. pygpt_net/controller/ctx/common.py +27 -17
  11. pygpt_net/controller/ctx/ctx.py +185 -101
  12. pygpt_net/controller/files/files.py +101 -41
  13. pygpt_net/controller/idx/indexer.py +87 -31
  14. pygpt_net/controller/kernel/kernel.py +13 -2
  15. pygpt_net/controller/mode/mode.py +3 -3
  16. pygpt_net/controller/model/editor.py +70 -15
  17. pygpt_net/controller/model/importer.py +153 -54
  18. pygpt_net/controller/painter/common.py +43 -11
  19. pygpt_net/controller/painter/painter.py +2 -2
  20. pygpt_net/controller/presets/experts.py +68 -15
  21. pygpt_net/controller/presets/presets.py +72 -36
  22. pygpt_net/controller/settings/profile.py +76 -35
  23. pygpt_net/controller/settings/workdir.py +70 -39
  24. pygpt_net/core/assistants/files.py +20 -18
  25. pygpt_net/core/filesystem/actions.py +111 -10
  26. pygpt_net/core/filesystem/filesystem.py +72 -1
  27. pygpt_net/core/filesystem/packer.py +161 -1
  28. pygpt_net/core/idx/idx.py +12 -11
  29. pygpt_net/core/idx/worker.py +13 -1
  30. pygpt_net/core/image/image.py +2 -2
  31. pygpt_net/core/models/models.py +4 -4
  32. pygpt_net/core/profile/profile.py +13 -3
  33. pygpt_net/core/video/video.py +2 -3
  34. pygpt_net/data/config/config.json +3 -3
  35. pygpt_net/data/config/models.json +3 -3
  36. pygpt_net/data/css/style.dark.css +45 -0
  37. pygpt_net/data/css/style.light.css +46 -0
  38. pygpt_net/data/locale/locale.de.ini +5 -1
  39. pygpt_net/data/locale/locale.en.ini +5 -1
  40. pygpt_net/data/locale/locale.es.ini +5 -1
  41. pygpt_net/data/locale/locale.fr.ini +5 -1
  42. pygpt_net/data/locale/locale.it.ini +5 -1
  43. pygpt_net/data/locale/locale.pl.ini +6 -2
  44. pygpt_net/data/locale/locale.uk.ini +5 -1
  45. pygpt_net/data/locale/locale.zh.ini +5 -1
  46. pygpt_net/provider/api/openai/__init__.py +4 -2
  47. pygpt_net/provider/core/config/patch.py +17 -1
  48. pygpt_net/tools/image_viewer/tool.py +17 -0
  49. pygpt_net/tools/text_editor/tool.py +9 -0
  50. pygpt_net/ui/__init__.py +2 -2
  51. pygpt_net/ui/dialog/preset.py +1 -0
  52. pygpt_net/ui/layout/ctx/ctx_list.py +16 -6
  53. pygpt_net/ui/layout/toolbox/image.py +2 -1
  54. pygpt_net/ui/layout/toolbox/indexes.py +2 -0
  55. pygpt_net/ui/layout/toolbox/video.py +5 -1
  56. pygpt_net/ui/main.py +3 -1
  57. pygpt_net/ui/widget/calendar/select.py +3 -3
  58. pygpt_net/ui/widget/draw/painter.py +238 -51
  59. pygpt_net/ui/widget/filesystem/explorer.py +1164 -142
  60. pygpt_net/ui/widget/lists/assistant.py +185 -24
  61. pygpt_net/ui/widget/lists/assistant_store.py +245 -42
  62. pygpt_net/ui/widget/lists/attachment.py +230 -47
  63. pygpt_net/ui/widget/lists/attachment_ctx.py +189 -33
  64. pygpt_net/ui/widget/lists/base_list_combo.py +2 -2
  65. pygpt_net/ui/widget/lists/context.py +1253 -70
  66. pygpt_net/ui/widget/lists/experts.py +110 -8
  67. pygpt_net/ui/widget/lists/model_editor.py +217 -14
  68. pygpt_net/ui/widget/lists/model_importer.py +125 -6
  69. pygpt_net/ui/widget/lists/preset.py +460 -71
  70. pygpt_net/ui/widget/lists/profile.py +149 -27
  71. pygpt_net/ui/widget/lists/uploaded.py +230 -38
  72. pygpt_net/ui/widget/option/combo.py +1211 -33
  73. pygpt_net/ui/widget/option/dictionary.py +35 -7
  74. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/METADATA +22 -57
  75. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/RECORD +78 -78
  76. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/LICENSE +0 -0
  77. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/WHEEL +0 -0
  78. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/entry_points.txt +0 -0
@@ -6,13 +6,14 @@
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.12.27 21:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
13
13
  import os
14
14
  from typing import List, Dict, Optional, Any
15
15
 
16
+ from PySide6 import QtCore
16
17
  from PySide6.QtWidgets import QApplication
17
18
 
18
19
  from pygpt_net.core.events import Event
@@ -45,8 +46,10 @@ class Importer:
45
46
  self.items_current = {} # current models in use
46
47
  self.pending = {} # waiting to be imported models
47
48
  self.removed = {} # waiting to be removed models
48
- self.selected_available = None # selected available model
49
- self.selected_current = None # selected current model
49
+ self.selected_available = None # selected available model (single)
50
+ self.selected_current = None # selected current model (single)
51
+ self.selected_available_ids: List[str] = [] # multi-selected available models
52
+ self.selected_current_ids: List[str] = [] # multi-selected current models
50
53
  self.all = False # show all models, not only available for import
51
54
  self.provider = "_" # default provider
52
55
 
@@ -69,43 +72,71 @@ class Importer:
69
72
 
70
73
  def change_available(self):
71
74
  """On change available model selection"""
72
- val = self.window.ui.nodes["models.importer.available"].selectionModel().currentIndex()
73
- idx = val.row()
74
- if idx < 0:
75
+ view = self.window.ui.nodes["models.importer.available"]
76
+ sel_model = view.selectionModel()
77
+ rows = sel_model.selectedRows() if sel_model else []
78
+ # collect selected model keys by reading stored tooltip role (stable ID)
79
+ selected_ids = []
80
+ for ix in rows:
81
+ key = ix.data(QtCore.Qt.ToolTipRole)
82
+ if not key:
83
+ key = self.get_by_idx(ix.row(), self.items_available)
84
+ if key and key not in selected_ids:
85
+ selected_ids.append(key)
86
+
87
+ self.selected_available_ids = selected_ids
88
+ if not selected_ids:
75
89
  self.selected_available = None
76
90
  self.window.ui.nodes["models.importer.add"].setEnabled(False)
77
- else:
78
- self.selected_available = self.get_by_idx(idx, self.items_available)
79
- if self.items_available.get(self.selected_available) is None:
80
- self.selected_available = None
81
- self.window.ui.nodes["models.importer.add"].setEnabled(False)
82
- else:
83
- # if not in current then enable add button
84
- if not self.in_current(self.selected_available):
85
- self.window.ui.nodes["models.importer.add"].setEnabled(True)
86
- else:
87
- self.window.ui.nodes["models.importer.add"].setEnabled(False)
91
+ return
92
+
93
+ # keep a single reference for backward compatibility (last selected)
94
+ self.selected_available = selected_ids[-1]
95
+
96
+ # enable add if any of selected can be added (not already in current)
97
+ can_add = False
98
+ for key in selected_ids:
99
+ if key is None:
100
+ continue
101
+ if not self.in_current(key) and self.items_available.get(key) is not None:
102
+ can_add = True
103
+ break
104
+ self.window.ui.nodes["models.importer.add"].setEnabled(can_add)
88
105
 
89
106
  def change_current(self):
90
107
  """On change current model selection"""
91
108
  if self.provider not in self.items_current:
92
109
  self.items_current[self.provider] = {}
93
- val = self.window.ui.nodes["models.importer.current"].selectionModel().currentIndex()
94
- idx = val.row()
95
- if idx < 0:
110
+ view = self.window.ui.nodes["models.importer.current"]
111
+ sel_model = view.selectionModel()
112
+ rows = sel_model.selectedRows() if sel_model else []
113
+ # collect selected model keys by reading stored tooltip role (stable ID)
114
+ selected_ids = []
115
+ for ix in rows:
116
+ key = ix.data(QtCore.Qt.ToolTipRole)
117
+ if not key:
118
+ key = self.get_by_idx(ix.row(), self.items_current[self.provider])
119
+ if key and key not in selected_ids:
120
+ selected_ids.append(key)
121
+
122
+ self.selected_current_ids = selected_ids
123
+ if not selected_ids:
96
124
  self.selected_current = None
97
125
  self.window.ui.nodes["models.importer.remove"].setEnabled(False)
98
- else:
99
- self.selected_current = self.get_by_idx(idx, self.items_current[self.provider])
100
- if self.items_current[self.provider].get(self.selected_current) is None:
101
- self.selected_current = None
102
- self.window.ui.nodes["models.importer.remove"].setEnabled(False)
103
- else:
104
- if (self.selected_current in self.items_current[self.provider]
105
- and self.in_available(self.selected_current)):
106
- self.window.ui.nodes["models.importer.remove"].setEnabled(True)
107
- else:
108
- self.window.ui.nodes["models.importer.remove"].setEnabled(False)
126
+ return
127
+
128
+ # keep a single reference for backward compatibility (last selected)
129
+ self.selected_current = selected_ids[-1]
130
+
131
+ # enable remove if any of selected can be removed
132
+ can_remove = False
133
+ for key in selected_ids:
134
+ if key is None:
135
+ continue
136
+ if (key in self.items_current[self.provider]) and self.in_available(key):
137
+ can_remove = True
138
+ break
139
+ self.window.ui.nodes["models.importer.remove"].setEnabled(can_remove)
109
140
 
110
141
  def in_available(self, model: str) -> bool:
111
142
  """
@@ -122,43 +153,111 @@ class Importer:
122
153
  return True
123
154
  return False
124
155
 
156
+ def _selected_keys_from_view(self, node_id: str, items: Dict) -> List[str]:
157
+ """
158
+ Resolve selected model keys from a given list view using stored tooltip role,
159
+ falling back to index->dict mapping when needed.
160
+
161
+ :param node_id: ui node id
162
+ :param items: dict used to build the view's model
163
+ :return: list of selected keys
164
+ """
165
+ keys: List[str] = []
166
+ try:
167
+ view = self.window.ui.nodes[node_id]
168
+ sel_model = view.selectionModel()
169
+ rows = sel_model.selectedRows() if sel_model else []
170
+ for ix in rows:
171
+ key = ix.data(QtCore.Qt.ToolTipRole)
172
+ if not key:
173
+ key = self.get_by_idx(ix.row(), items)
174
+ if key and key not in keys:
175
+ keys.append(key)
176
+ except Exception:
177
+ pass
178
+ return keys
179
+
125
180
  def add(self):
126
- """Add model to current list"""
181
+ """Add model(s) to current list"""
127
182
  if self.provider not in self.items_current:
128
183
  self.items_current[self.provider] = {}
129
- if self.selected_available is None:
184
+
185
+ # collect multi-selection; fallback to single selection
186
+ keys = self._selected_keys_from_view(
187
+ 'models.importer.available',
188
+ self.items_available,
189
+ )
190
+ if not keys and self.selected_available is not None:
191
+ keys = [self.selected_available]
192
+
193
+ if not keys:
130
194
  self.set_status(trans('models.importer.error.add.no_model'))
131
195
  return
132
- if self.in_current(self.selected_available):
196
+
197
+ any_added = False
198
+ for key in list(keys):
199
+ if key is None:
200
+ continue
201
+ if self.in_current(key):
202
+ continue
203
+ model = self.items_available.get(key)
204
+ if model is None:
205
+ continue
206
+ self.items_current[self.provider][key] = model
207
+ if key not in self.pending:
208
+ self.pending[key] = model
209
+ if key in self.removed:
210
+ del self.removed[key]
211
+ if not self.all and key in self.items_available:
212
+ del self.items_available[key]
213
+ any_added = True
214
+
215
+ if not any_added:
133
216
  self.set_status(trans('models.importer.error.add.not_exists'))
134
217
  return
135
- model = self.items_available[self.selected_available]
136
- self.items_current[self.provider][self.selected_available] = model
137
- if self.selected_available not in self.pending:
138
- self.pending[self.selected_available] = model
139
- if self.selected_available in self.removed:
140
- del self.removed[self.selected_available]
141
- if not self.all:
142
- del self.items_available[self.selected_available]
218
+
143
219
  self.refresh()
144
220
 
145
221
  def remove(self):
146
- """Remove model from current list"""
222
+ """Remove model(s) from current list"""
147
223
  if self.provider not in self.items_current:
148
224
  self.items_current[self.provider] = {}
149
- if self.selected_current is None:
225
+
226
+ # collect multi-selection; fallback to single selection
227
+ keys = self._selected_keys_from_view(
228
+ 'models.importer.current',
229
+ self.items_current[self.provider],
230
+ )
231
+ if not keys and self.selected_current is not None:
232
+ keys = [self.selected_current]
233
+
234
+ if not keys:
150
235
  self.set_status(trans('models.importer.error.remove.no_model'))
151
236
  return
152
- if not self.in_current(self.selected_current):
237
+
238
+ any_removed = False
239
+ for key in list(keys):
240
+ if key is None:
241
+ continue
242
+ if not self.in_current(key):
243
+ continue
244
+ model = self.items_current[self.provider].get(key)
245
+ if model is None:
246
+ continue
247
+ # return to available list
248
+ self.items_available[key] = model
249
+ if key not in self.removed:
250
+ self.removed[key] = model
251
+ if key in self.items_current[self.provider]:
252
+ del self.items_current[self.provider][key]
253
+ if key in self.pending:
254
+ del self.pending[key]
255
+ any_removed = True
256
+
257
+ if not any_removed:
153
258
  self.set_status(trans('models.importer.error.remove.not_exists'))
154
259
  return
155
- model = self.items_current[self.provider][self.selected_current]
156
- self.items_available[self.selected_current] = model
157
- if self.selected_current not in self.removed:
158
- self.removed[self.selected_current] = model
159
- del self.items_current[self.provider][self.selected_current]
160
- if self.selected_current in self.pending:
161
- del self.pending[self.selected_current]
260
+
162
261
  self.refresh()
163
262
 
164
263
  def setup(self):
@@ -328,7 +427,7 @@ class Importer:
328
427
  name = model.get('id')
329
428
  if "name" in model:
330
429
  name = model.get('name')
331
- m = self.window.core.models.create_empty(append=False)
430
+ m, _ = self.window.core.models.create_empty(append=False)
332
431
  m.id = id
333
432
  m.name = name
334
433
  m.mode = [
@@ -504,7 +603,7 @@ class Importer:
504
603
  else:
505
604
  for model in ollama_models:
506
605
  name = model.get('name')
507
- m = self.window.core.models.create_empty(append=False)
606
+ m, _ = self.window.core.models.create_empty(append=False)
508
607
  m.id = name
509
608
  m.name = name
510
609
  m.mode = [
@@ -47,7 +47,12 @@ class Common:
47
47
  :param width: Canvas width
48
48
  :param height: Canvas height
49
49
  """
50
- self.window.ui.painter.setFixedSize(QSize(width, height))
50
+ painter = self.window.ui.painter
51
+ if hasattr(painter, "set_canvas_size_pixels"):
52
+ painter.set_canvas_size_pixels(width, height)
53
+ else:
54
+ # required on image open
55
+ self.window.ui.painter.setFixedSize(QSize(width, height))
51
56
 
52
57
  def set_brush_mode(self, enabled: bool):
53
58
  """
@@ -81,14 +86,21 @@ class Common:
81
86
  if self._changing_canvas_size:
82
87
  return
83
88
 
84
- combo: QComboBox = self.window.ui.nodes['painter.select.canvas.size']
89
+ # Be resilient if combobox node is not present in a given UI layout
90
+ combo: Optional[QComboBox] = None
91
+ try:
92
+ if hasattr(self.window.ui, "nodes"):
93
+ combo = self.window.ui.nodes.get('painter.select.canvas.size', None)
94
+ except Exception:
95
+ combo = None
96
+
85
97
  painter = self.window.ui.painter
86
98
 
87
99
  # Heuristic to detect manual UI change vs programmatic call
88
100
  # - manual if: no arg, or int index (Qt int overload), or arg equals currentText/currentData
89
101
  raw_arg = selected
90
- current_text = combo.currentText()
91
- current_data = combo.currentData()
102
+ current_text = combo.currentText() if combo is not None else ""
103
+ current_data = combo.currentData() if combo is not None else None
92
104
  current_data_str = current_data if isinstance(current_data, str) else None
93
105
  is_manual = (
94
106
  raw_arg is None
@@ -105,8 +117,15 @@ class Common:
105
117
  if not selected_norm:
106
118
  return
107
119
 
120
+ # Use true logical canvas size when available
121
+ if hasattr(painter, "get_canvas_size"):
122
+ cur_sz = painter.get_canvas_size()
123
+ cur_val = f"{cur_sz.width()}x{cur_sz.height()}"
124
+ else:
125
+ cur_val = f"{painter.width()}x{painter.height()}"
126
+
108
127
  # Save undo only for manual changes and only if size will change
109
- will_change = selected_norm != f"{painter.width()}x{painter.height()}"
128
+ will_change = selected_norm != cur_val
110
129
  if is_manual and will_change:
111
130
  painter.saveForUndo()
112
131
 
@@ -124,9 +143,10 @@ class Common:
124
143
  self._sticky_custom_value = selected_norm
125
144
 
126
145
  # Ensure combo reflects single custom at index 0 (sticky respected), then select current value
127
- self._sync_canvas_size_combo(combo, selected_norm, sticky_to_keep=self._sticky_custom_value)
146
+ if combo is not None:
147
+ self._sync_canvas_size_combo(combo, selected_norm, sticky_to_keep=self._sticky_custom_value)
128
148
 
129
- # Apply canvas size; PainterWidget handles rescaling in resizeEvent
149
+ # Apply canvas size; PainterWidget handles rescaling in its own logic
130
150
  w, h = self.convert_to_size(selected_norm)
131
151
  self.set_canvas_size(w, h)
132
152
 
@@ -272,10 +292,22 @@ class Common:
272
292
  if self._changing_canvas_size:
273
293
  return
274
294
 
275
- combo: QComboBox = self.window.ui.nodes['painter.select.canvas.size']
295
+ combo: Optional[QComboBox] = None
296
+ try:
297
+ if hasattr(self.window.ui, "nodes"):
298
+ combo = self.window.ui.nodes.get('painter.select.canvas.size', None)
299
+ except Exception:
300
+ combo = None
301
+
276
302
  painter = self.window.ui.painter
277
303
 
278
- canvas_value = f"{painter.width()}x{painter.height()}"
304
+ # Use true logical canvas size, not widget size
305
+ if hasattr(painter, "get_canvas_size"):
306
+ sz = painter.get_canvas_size()
307
+ canvas_value = f"{sz.width()}x{sz.height()}"
308
+ else:
309
+ canvas_value = f"{painter.width()}x{painter.height()}"
310
+
279
311
  canvas_norm = self._normalize_canvas_value(canvas_value)
280
312
  if not canvas_norm:
281
313
  return
@@ -292,7 +324,8 @@ class Common:
292
324
  try:
293
325
  self._changing_canvas_size = True
294
326
  self._sticky_custom_value = sticky
295
- self._sync_canvas_size_combo(combo, canvas_norm, sticky_to_keep=sticky)
327
+ if combo is not None:
328
+ self._sync_canvas_size_combo(combo, canvas_norm, sticky_to_keep=sticky)
296
329
 
297
330
  # Persist canvas size only (do not change sticky config-scope)
298
331
  self.window.core.config.set('painter.canvas.size', canvas_norm)
@@ -409,7 +442,6 @@ class Common:
409
442
  combo.setCurrentText(value)
410
443
  else:
411
444
  # Current value is custom: ensure it exists at index 0 and select it
412
- # If sticky differs or is None, overwrite/create the custom at index 0 to reflect true current value.
413
445
  if not sticky_to_keep or sticky_to_keep != value:
414
446
  self._ensure_custom_index0(combo, value, predef)
415
447
  if combo.currentIndex() != 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.09.02 15:00:00 #
9
+ # Updated Date: 2025.12.27 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -53,7 +53,7 @@ class Painter:
53
53
  """
54
54
  self.open(path)
55
55
  if not self.is_active():
56
- self.window.controller.ui.switch_tab('draw')
56
+ self.window.controller.ui.tabs.switch_tab(Tab.TAB_TOOL_PAINTER)
57
57
 
58
58
  def save(self):
59
59
  """Store current image"""
@@ -6,9 +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.14 13:00:00 #
9
+ # Updated Date: 2025.12.27 21:00:00 #
10
10
  # ================================================== #
11
11
 
12
+ from PySide6 import QtCore
13
+
12
14
  from pygpt_net.core.types import (
13
15
  MODE_EXPERT,
14
16
  )
@@ -23,6 +25,8 @@ class Experts:
23
25
  :param window: Window instance
24
26
  """
25
27
  self.window = window
28
+ self.selected_available_uuids = [] # multi-selected available experts
29
+ self.selected_selected_uuids = [] # multi-selected selected experts
26
30
 
27
31
  def refresh(self):
28
32
  """Refresh presets"""
@@ -78,13 +82,43 @@ class Experts:
78
82
 
79
83
  self.update_tab()
80
84
 
85
+ def _selected_uuids_from_view(self, node_id: str, fallback_idx_resolver) -> list[str]:
86
+ """
87
+ Resolve selected expert UUIDs from a given list view using stored tooltip role,
88
+ falling back to idx -> uuid resolver when needed.
89
+
90
+ :param node_id: ui node id
91
+ :param fallback_idx_resolver: callable that maps row index -> uuid
92
+ :return: list of selected uuids
93
+ """
94
+ uuids = []
95
+ try:
96
+ view = self.window.ui.nodes[node_id]
97
+ sel_model = view.selectionModel()
98
+ rows = sel_model.selectedRows() if sel_model else []
99
+ for ix in rows:
100
+ uuid = ix.data(QtCore.Qt.ToolTipRole)
101
+ if not uuid:
102
+ uuid = fallback_idx_resolver(ix.row())
103
+ if uuid and uuid not in uuids:
104
+ uuids.append(uuid)
105
+ except Exception:
106
+ pass
107
+ return uuids
108
+
81
109
  def change_available(self):
82
- """Change selected expert"""
83
- pass
110
+ """Change selected expert(s) in available list"""
111
+ self.selected_available_uuids = self._selected_uuids_from_view(
112
+ "preset.experts.available",
113
+ self.get_available_by_idx
114
+ )
84
115
 
85
116
  def change_selected(self):
86
- """Change selected expert"""
87
- pass
117
+ """Change selected expert(s) in selected list"""
118
+ self.selected_selected_uuids = self._selected_uuids_from_view(
119
+ "preset.experts.selected",
120
+ self.get_selected_by_idx
121
+ )
88
122
 
89
123
  def get_current_available(self) -> str:
90
124
  """
@@ -138,27 +172,47 @@ class Experts:
138
172
  i += 1
139
173
 
140
174
  def add_expert(self):
141
- """Add expert"""
175
+ """Add expert(s)"""
142
176
  agent_uuid = self.get_current_agent_id()
143
177
  if agent_uuid is None or not self.window.core.presets.exists_uuid(agent_uuid):
144
178
  self.window.controller.presets.editor.save(close=False)
145
179
  return
146
- expert_uuid = self.get_current_available()
147
- if expert_uuid is None:
180
+
181
+ uuids = self.selected_available_uuids[:]
182
+ if not uuids:
183
+ one = self.get_current_available()
184
+ if one:
185
+ uuids = [one]
186
+
187
+ if not uuids:
148
188
  return
149
- self.window.core.presets.add_expert(agent_uuid, expert_uuid)
189
+
190
+ for expert_uuid in uuids:
191
+ if expert_uuid and not self.is_active(expert_uuid):
192
+ self.window.core.presets.add_expert(agent_uuid, expert_uuid)
193
+
150
194
  self.update_list()
151
195
 
152
196
  def remove_expert(self):
153
- """Remove expert"""
197
+ """Remove expert(s)"""
154
198
  agent_uuid = self.get_current_agent_id()
155
199
  if agent_uuid is None or not self.window.core.presets.exists_uuid(agent_uuid):
156
200
  self.window.controller.presets.editor.save(close=False)
157
201
  return
158
- expert_uuid = self.get_current_selected()
159
- if expert_uuid is None:
202
+
203
+ uuids = self.selected_selected_uuids[:]
204
+ if not uuids:
205
+ one = self.get_current_selected()
206
+ if one:
207
+ uuids = [one]
208
+
209
+ if not uuids:
160
210
  return
161
- self.window.core.presets.remove_expert(agent_uuid, expert_uuid)
211
+
212
+ for expert_uuid in uuids:
213
+ if expert_uuid and self.is_active(expert_uuid):
214
+ self.window.core.presets.remove_expert(agent_uuid, expert_uuid)
215
+
162
216
  self.update_list()
163
217
 
164
218
  def update_tab(self):
@@ -174,5 +228,4 @@ class Experts:
174
228
  if num == 0:
175
229
  tabs.setTabText(idx, trans("preset.tab.experts"))
176
230
  else:
177
- tabs.setTabText(idx, trans("preset.tab.experts") + f" ({num})")
178
-
231
+ tabs.setTabText(idx, trans("preset.tab.experts") + f" ({num})")