pygpt-net 2.6.66__py3-none-any.whl → 2.7.0__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 (81) hide show
  1. pygpt_net/CHANGELOG.txt +18 -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/config/field/textarea.py +2 -2
  11. pygpt_net/controller/ctx/common.py +27 -17
  12. pygpt_net/controller/ctx/ctx.py +182 -101
  13. pygpt_net/controller/dialogs/info.py +2 -2
  14. pygpt_net/controller/files/files.py +101 -41
  15. pygpt_net/controller/idx/indexer.py +87 -31
  16. pygpt_net/controller/kernel/kernel.py +13 -2
  17. pygpt_net/controller/media/media.py +29 -1
  18. pygpt_net/controller/mode/mode.py +3 -3
  19. pygpt_net/controller/model/editor.py +141 -21
  20. pygpt_net/controller/model/importer.py +153 -54
  21. pygpt_net/controller/painter/painter.py +2 -2
  22. pygpt_net/controller/presets/experts.py +68 -15
  23. pygpt_net/controller/presets/presets.py +72 -36
  24. pygpt_net/controller/settings/editor.py +25 -1
  25. pygpt_net/controller/settings/profile.py +76 -35
  26. pygpt_net/controller/settings/workdir.py +70 -39
  27. pygpt_net/core/assistants/files.py +20 -18
  28. pygpt_net/core/filesystem/actions.py +111 -10
  29. pygpt_net/core/filesystem/filesystem.py +2 -1
  30. pygpt_net/core/idx/idx.py +12 -11
  31. pygpt_net/core/idx/worker.py +13 -1
  32. pygpt_net/core/models/models.py +4 -4
  33. pygpt_net/core/profile/profile.py +13 -3
  34. pygpt_net/core/types/image.py +10 -1
  35. pygpt_net/core/video/video.py +43 -3
  36. pygpt_net/data/config/config.json +3 -3
  37. pygpt_net/data/config/models.json +25 -14
  38. pygpt_net/data/css/style.dark.css +39 -1
  39. pygpt_net/data/css/style.light.css +39 -1
  40. pygpt_net/data/locale/locale.de.ini +4 -1
  41. pygpt_net/data/locale/locale.en.ini +4 -1
  42. pygpt_net/data/locale/locale.es.ini +4 -1
  43. pygpt_net/data/locale/locale.fr.ini +4 -1
  44. pygpt_net/data/locale/locale.it.ini +4 -1
  45. pygpt_net/data/locale/locale.pl.ini +5 -2
  46. pygpt_net/data/locale/locale.uk.ini +4 -1
  47. pygpt_net/data/locale/locale.zh.ini +4 -1
  48. pygpt_net/item/model.py +1 -1
  49. pygpt_net/provider/api/openai/__init__.py +4 -2
  50. pygpt_net/provider/api/openai/video.py +2 -2
  51. pygpt_net/provider/core/config/patch.py +9 -1
  52. pygpt_net/provider/core/model/patch.py +26 -1
  53. pygpt_net/tools/image_viewer/tool.py +17 -0
  54. pygpt_net/tools/text_editor/tool.py +9 -0
  55. pygpt_net/ui/__init__.py +2 -2
  56. pygpt_net/ui/dialog/models.py +10 -1
  57. pygpt_net/ui/layout/ctx/ctx_list.py +16 -6
  58. pygpt_net/ui/layout/toolbox/video.py +14 -6
  59. pygpt_net/ui/main.py +3 -1
  60. pygpt_net/ui/widget/calendar/select.py +3 -3
  61. pygpt_net/ui/widget/filesystem/explorer.py +1082 -142
  62. pygpt_net/ui/widget/lists/assistant.py +185 -24
  63. pygpt_net/ui/widget/lists/assistant_store.py +245 -42
  64. pygpt_net/ui/widget/lists/attachment.py +230 -47
  65. pygpt_net/ui/widget/lists/attachment_ctx.py +189 -33
  66. pygpt_net/ui/widget/lists/base_list_combo.py +2 -2
  67. pygpt_net/ui/widget/lists/context.py +1253 -70
  68. pygpt_net/ui/widget/lists/experts.py +110 -8
  69. pygpt_net/ui/widget/lists/model_editor.py +217 -14
  70. pygpt_net/ui/widget/lists/model_importer.py +125 -6
  71. pygpt_net/ui/widget/lists/preset.py +460 -71
  72. pygpt_net/ui/widget/lists/profile.py +149 -27
  73. pygpt_net/ui/widget/lists/uploaded.py +230 -38
  74. pygpt_net/ui/widget/option/combo.py +1046 -32
  75. pygpt_net/ui/widget/option/dictionary.py +35 -7
  76. pygpt_net/ui/widget/option/input.py +3 -1
  77. {pygpt_net-2.6.66.dist-info → pygpt_net-2.7.0.dist-info}/METADATA +20 -57
  78. {pygpt_net-2.6.66.dist-info → pygpt_net-2.7.0.dist-info}/RECORD +81 -81
  79. {pygpt_net-2.6.66.dist-info → pygpt_net-2.7.0.dist-info}/LICENSE +0 -0
  80. {pygpt_net-2.6.66.dist-info → pygpt_net-2.7.0.dist-info}/WHEEL +0 -0
  81. {pygpt_net-2.6.66.dist-info → pygpt_net-2.7.0.dist-info}/entry_points.txt +0 -0
@@ -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})")
@@ -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.25 20:00:00 #
9
+ # Updated Date: 2025.12.27 21:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import re
13
- from typing import Optional, List, Dict
13
+ from typing import Optional, List, Dict, Union
14
14
 
15
15
  from PySide6.QtCore import QTimer
16
16
  from PySide6.QtGui import QTextCursor
@@ -536,25 +536,39 @@ class Presets:
536
536
  """
537
537
  return _FILENAME_SANITIZE_RE.sub('_', name.lower())
538
538
 
539
- def duplicate(self, idx: Optional[int] = None):
539
+ def duplicate(self, idx: Optional[Union[int, list]] = None):
540
540
  """
541
541
  Duplicate preset
542
542
 
543
- :param idx: preset index (row index)
543
+ :param idx: preset ID (or list of IDs)
544
544
  """
545
- if idx is not None:
546
- w = self.window
547
- mode = w.core.config.get('mode')
548
- preset = w.core.presets.get_by_idx(idx, mode)
549
- if preset:
550
- if preset in w.core.presets.items:
551
- new_id = w.core.presets.duplicate(preset)
552
- self.update_list()
553
- self.refresh(no_scroll=True)
554
- idx = w.core.presets.get_idx_by_id(mode, new_id)
555
- self.editor.edit(idx)
556
- self.select(idx) # switch to the new preset
557
- w.update_status(trans('status.preset.duplicated'))
545
+ ids = idx if isinstance(idx, list) else [idx]
546
+ last_id = None
547
+ updated = False
548
+ w = self.window
549
+ mode = w.core.config.get('mode')
550
+ for idx in ids:
551
+ if idx is not None:
552
+ if len(ids) > 1:
553
+ preset = w.core.presets.get_by_id(mode, idx) # by ID, PresetItem
554
+ preset_id = preset.filename if preset else None
555
+ else:
556
+ preset = w.core.presets.get_by_idx(idx, mode) # by index, str
557
+ preset_id = preset
558
+ if preset_id:
559
+ new_id = w.core.presets.duplicate(preset_id)
560
+ last_id = new_id
561
+ updated = True
562
+
563
+ if updated:
564
+ self.update_list()
565
+ self.refresh(no_scroll=True)
566
+ if len(ids) == 1 and idx is not None:
567
+ idx = w.core.presets.get_idx_by_id(mode, last_id)
568
+ self.editor.edit(idx)
569
+ self.select(idx) # switch to the new preset if only one was duplicated
570
+ w.update_status(trans('status.preset.duplicated'))
571
+
558
572
 
559
573
  def enable(self, idx: Optional[int] = None):
560
574
  """
@@ -654,30 +668,49 @@ class Presets:
654
668
 
655
669
  def delete(
656
670
  self,
657
- idx: Optional[int] = None,
671
+ idx: Optional[Union[int, list]] = None,
658
672
  force: bool = False
659
673
  ):
660
674
  """
661
675
  Delete preset
662
676
 
663
- :param idx: preset index (row index)
677
+ :param idx: preset index (row index) or list of indices
664
678
  :param force: force delete without confirmation
665
679
  """
666
- if idx is not None:
667
- w = self.window
668
- mode = w.core.config.get('mode')
669
- preset_id = w.core.presets.get_by_idx(idx, mode)
670
- if preset_id and preset_id in w.core.presets.items:
671
- if not force:
672
- w.ui.dialogs.confirm(
673
- type='preset_delete',
674
- id=idx,
675
- msg=trans('confirm.preset.delete'),
676
- )
677
- return
678
-
679
- # Determine neighbor only if the deleted preset is currently active.
680
- # This keeps API semantics untouched and prevents unexpected selection changes.
680
+ ids = idx if isinstance(idx, list) else [idx]
681
+ w = self.window
682
+ mode = w.core.config.get('mode')
683
+ is_preset = False
684
+ updated = False
685
+ for tmp_id in ids:
686
+ if len(ids) > 1:
687
+ preset = w.core.presets.get_by_id(mode, tmp_id) # by ID, PresetItem
688
+ else:
689
+ preset = w.core.presets.get_by_idx(tmp_id, mode) # by index, str
690
+ if preset:
691
+ is_preset = True
692
+ break
693
+
694
+ if ids and is_preset:
695
+ if not force:
696
+ w.ui.dialogs.confirm(
697
+ type='preset_delete',
698
+ id=idx,
699
+ msg=trans('confirm.preset.delete'),
700
+ )
701
+ return
702
+
703
+ # Determine neighbor only if the deleted preset is currently active.
704
+ # This keeps API semantics untouched and prevents unexpected selection changes.
705
+ for idx in ids:
706
+ if len(ids) > 1:
707
+ preset = w.core.presets.get_by_id(mode, idx) # by ID, PresetItem
708
+ preset_id = preset.filename if preset else None
709
+ else:
710
+ preset = w.core.presets.get_by_idx(idx, mode) # by index, str
711
+ preset_id = preset
712
+ if not preset:
713
+ continue
681
714
  is_current = (preset_id == w.core.config.get('preset'))
682
715
  target_id = None
683
716
  if is_current:
@@ -700,8 +733,11 @@ class Presets:
700
733
  w.core.config.set('preset', None)
701
734
  w.ui.nodes['preset.prompt'].setPlainText("")
702
735
 
703
- self.refresh(no_scroll=True)
704
- w.update_status(trans('status.preset.deleted'))
736
+ updated = True
737
+
738
+ if updated:
739
+ self.refresh(no_scroll=True)
740
+ w.update_status(trans('status.preset.deleted'))
705
741
 
706
742
  def restore(self, force: bool = False):
707
743
  """
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.15 22:00:00 #
9
+ # Updated Date: 2025.12.26 12:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
@@ -206,6 +206,30 @@ class Editor:
206
206
  if self.config_changed('access.shortcuts'):
207
207
  self.window.setup_global_shortcuts()
208
208
 
209
+ # video: resolution
210
+ if self.config_changed('video.resolution'):
211
+ value = self.window.core.config.get('video.resolution')
212
+ self.window.core.config.set('video.resolution', value)
213
+ option = self.window.core.video.get_resolution_option()
214
+ self.window.controller.config.apply_value(
215
+ parent_id='global',
216
+ key='video.resolution',
217
+ option=option,
218
+ value=str(value),
219
+ )
220
+
221
+ # video: duration
222
+ if self.config_changed('video.duration'):
223
+ value = self.window.core.config.get('video.duration')
224
+ self.window.core.config.set('video.duration', value)
225
+ option = self.window.core.video.get_duration_option()
226
+ self.window.controller.config.apply_value(
227
+ parent_id='global',
228
+ key='video.duration',
229
+ option=option,
230
+ value=int(value) or 8,
231
+ )
232
+
209
233
  # update ENV
210
234
  self.window.core.config.setup_env()
211
235
 
@@ -6,12 +6,12 @@
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.26 13:00:00 #
9
+ # Updated Date: 2025.12.28 04:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
13
13
  from pathlib import Path
14
- from typing import Optional, Dict, Any
14
+ from typing import Optional, Dict, Any, Union
15
15
 
16
16
  from PySide6.QtCore import Slot, QTimer
17
17
  from PySide6.QtGui import QAction
@@ -173,6 +173,8 @@ class Profile:
173
173
  self.setup()
174
174
  self.initialized = True
175
175
  if not self.dialog or force:
176
+ self.update_menu()
177
+ self.update_list()
176
178
  self.window.ui.dialogs.open(
177
179
  'profile.editor',
178
180
  width=self.width,
@@ -336,18 +338,26 @@ class Profile:
336
338
  uuid = self.get_id_by_idx(idx)
337
339
  self.switch(uuid)
338
340
 
339
- def delete_by_idx(self, idx: int, force: bool = False):
341
+ def delete_by_idx(self, idx: Union[int, list], force: bool = False):
340
342
  """
341
343
  Delete profile by index
342
344
 
343
- :param idx: profile index
345
+ :param idx: profile index or list of indexes
344
346
  :param force: force delete
345
347
  """
346
- uuid = self.get_id_by_idx(idx)
348
+ uuids = []
349
+ ids = idx if isinstance(idx, list) else [idx]
350
+ for i in ids:
351
+ uuid = self.get_id_by_idx(i)
352
+ uuids.append(uuid)
347
353
  current = self.window.core.config.profile.get_current()
348
- if uuid == current:
349
- self.window.ui.dialogs.alert(trans("dialog.profile.alert.delete.current"))
350
- return
354
+ for uuid in list(uuids):
355
+ if uuid == current:
356
+ if len(uuids) == 1:
357
+ self.window.ui.dialogs.alert(trans("dialog.profile.alert.delete.current"))
358
+ return
359
+ else:
360
+ uuids.remove(uuid) # skip current
351
361
  if not force:
352
362
  self.window.ui.dialogs.confirm(
353
363
  type='profile.delete',
@@ -355,35 +365,48 @@ class Profile:
355
365
  msg=trans('confirm.profile.delete'),
356
366
  )
357
367
  return
358
- self.delete(uuid)
368
+ self.delete(uuids)
359
369
 
360
- def delete(self, uuid: str):
370
+ def delete(self, uuid: Union[str, list]):
361
371
  """
362
372
  Delete profile (remove only)
363
373
 
364
- :param uuid: profile ID
374
+ :param uuid: profile ID or list of IDs
365
375
  """
366
376
  profiles = self.get_profiles()
367
- if uuid in profiles:
368
- profile = profiles[uuid]
369
- name = profile['name']
370
- if self.window.core.config.profile.remove(uuid):
371
- self.window.update_status(trans("dialog.profile.status.removed") + ": " + name)
372
- self.update_list()
373
- self.update_menu()
377
+ updated = False
378
+ ids = uuid if isinstance(uuid, list) else [uuid]
379
+ for uuid in ids:
380
+ if uuid in profiles:
381
+ profile = profiles[uuid]
382
+ name = profile['name']
383
+ if self.window.core.config.profile.remove(uuid):
384
+ updated = True
385
+ self.window.update_status(trans("dialog.profile.status.removed") + ": " + name)
386
+ if updated:
387
+ self.update_list()
388
+ self.update_menu()
374
389
 
375
- def delete_all_by_idx(self, idx: int, force: bool = False):
390
+ def delete_all_by_idx(self, idx: Union[int, list], force: bool = False):
376
391
  """
377
392
  Delete profile with files by index
378
393
 
379
- :param idx: profile index
394
+ :param idx: profile index or list of indexes
380
395
  :param force: force delete
381
396
  """
382
- uuid = self.get_id_by_idx(idx)
397
+ uuids = []
398
+ ids = idx if isinstance(idx, list) else [idx]
399
+ for i in ids:
400
+ uuid = self.get_id_by_idx(i)
401
+ uuids.append(uuid)
383
402
  current = self.window.core.config.profile.get_current()
384
- if uuid == current:
385
- self.window.ui.dialogs.alert(trans("dialog.profile.alert.delete.current"))
386
- return
403
+ for uuid in list(uuids):
404
+ if uuid == current:
405
+ if len(uuids) == 1:
406
+ self.window.ui.dialogs.alert(trans("dialog.profile.alert.delete.current"))
407
+ return
408
+ else:
409
+ uuids.remove(uuid) # skip current
387
410
  if not force:
388
411
  self.window.ui.dialogs.confirm(
389
412
  type='profile.delete.all',
@@ -391,7 +414,7 @@ class Profile:
391
414
  msg=trans('confirm.profile.delete_all'),
392
415
  )
393
416
  return
394
- self.delete_all(uuid)
417
+ self.delete_all(uuids)
395
418
 
396
419
  def duplicate_by_idx(self, idx: int):
397
420
  """
@@ -416,13 +439,20 @@ class Profile:
416
439
  dialog.prepare()
417
440
  dialog.show()
418
441
 
419
- def delete_all(self, uuid: str):
442
+ def delete_all(self, uuid: Union[str, list]):
420
443
  """
421
444
  Delete profile with files
422
445
 
423
- :param uuid: profile ID
446
+ :param uuid: profile ID or list of IDs
424
447
  """
425
- self.window.controller.settings.workdir.delete_files(uuid)
448
+ ids = uuid if isinstance(uuid, list) else [uuid]
449
+ batch = False
450
+ if len(ids) > 1:
451
+ batch = True
452
+ if not batch:
453
+ self.window.controller.settings.workdir.delete_files(uuid)
454
+ else:
455
+ self.window.controller.settings.workdir.delete_files(ids, batch=batch)
426
456
 
427
457
  @Slot(str)
428
458
  def after_delete(self, name: str):
@@ -468,22 +498,33 @@ class Profile:
468
498
  if self.window.ui.nodes['dialog.profile.checkbox.switch'].isChecked():
469
499
  self.switch(uuid, force=True)
470
500
 
471
- def reset(self, uuid: str):
501
+ def reset(self, uuid: Union[str, list]):
472
502
  """
473
503
  Reset profile
474
504
 
475
- :param uuid: profile ID
505
+ :param uuid: profile ID or list of IDs
476
506
  """
477
- self.window.controller.settings.workdir.reset(uuid)
507
+ ids = uuid if isinstance(uuid, list) else [uuid]
508
+ batch = False
509
+ if len(ids) > 1:
510
+ batch = True
511
+ if not batch:
512
+ self.window.controller.settings.workdir.reset(uuid)
513
+ else:
514
+ self.window.controller.settings.workdir.reset(ids, batch=batch)
478
515
 
479
- def reset_by_idx(self, idx: int, force: bool = False):
516
+ def reset_by_idx(self, idx: Union[int, list], force: bool = False):
480
517
  """
481
518
  Reset profile by index
482
519
 
483
- :param idx: profile index
520
+ :param idx: profile index or list of indexes
484
521
  :param force: force reset
485
522
  """
486
- uuid = self.get_id_by_idx(idx)
523
+ uuids = []
524
+ ids = idx if isinstance(idx, list) else [idx]
525
+ for i in ids:
526
+ uuid = self.get_id_by_idx(i)
527
+ uuids.append(uuid)
487
528
  if not force:
488
529
  self.window.ui.dialogs.confirm(
489
530
  type='profile.reset',
@@ -491,7 +532,7 @@ class Profile:
491
532
  msg=trans('confirm.profile.reset'),
492
533
  )
493
534
  return
494
- self.reset(uuid)
535
+ self.reset(uuids)
495
536
 
496
537
  def is_include_db(self):
497
538
  """Get include db"""