pygpt-net 2.6.67__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 (69) hide show
  1. pygpt_net/CHANGELOG.txt +12 -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 +182 -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/painter.py +2 -2
  19. pygpt_net/controller/presets/experts.py +68 -15
  20. pygpt_net/controller/presets/presets.py +72 -36
  21. pygpt_net/controller/settings/profile.py +76 -35
  22. pygpt_net/controller/settings/workdir.py +70 -39
  23. pygpt_net/core/assistants/files.py +20 -18
  24. pygpt_net/core/filesystem/actions.py +111 -10
  25. pygpt_net/core/filesystem/filesystem.py +2 -1
  26. pygpt_net/core/idx/idx.py +12 -11
  27. pygpt_net/core/idx/worker.py +13 -1
  28. pygpt_net/core/models/models.py +4 -4
  29. pygpt_net/core/profile/profile.py +13 -3
  30. pygpt_net/data/config/config.json +3 -3
  31. pygpt_net/data/config/models.json +3 -3
  32. pygpt_net/data/css/style.dark.css +39 -1
  33. pygpt_net/data/css/style.light.css +39 -1
  34. pygpt_net/data/locale/locale.de.ini +3 -1
  35. pygpt_net/data/locale/locale.en.ini +3 -1
  36. pygpt_net/data/locale/locale.es.ini +3 -1
  37. pygpt_net/data/locale/locale.fr.ini +3 -1
  38. pygpt_net/data/locale/locale.it.ini +3 -1
  39. pygpt_net/data/locale/locale.pl.ini +4 -2
  40. pygpt_net/data/locale/locale.uk.ini +3 -1
  41. pygpt_net/data/locale/locale.zh.ini +3 -1
  42. pygpt_net/provider/api/openai/__init__.py +4 -2
  43. pygpt_net/provider/core/config/patch.py +9 -1
  44. pygpt_net/tools/image_viewer/tool.py +17 -0
  45. pygpt_net/tools/text_editor/tool.py +9 -0
  46. pygpt_net/ui/__init__.py +2 -2
  47. pygpt_net/ui/layout/ctx/ctx_list.py +16 -6
  48. pygpt_net/ui/main.py +3 -1
  49. pygpt_net/ui/widget/calendar/select.py +3 -3
  50. pygpt_net/ui/widget/filesystem/explorer.py +1082 -142
  51. pygpt_net/ui/widget/lists/assistant.py +185 -24
  52. pygpt_net/ui/widget/lists/assistant_store.py +245 -42
  53. pygpt_net/ui/widget/lists/attachment.py +230 -47
  54. pygpt_net/ui/widget/lists/attachment_ctx.py +189 -33
  55. pygpt_net/ui/widget/lists/base_list_combo.py +2 -2
  56. pygpt_net/ui/widget/lists/context.py +1253 -70
  57. pygpt_net/ui/widget/lists/experts.py +110 -8
  58. pygpt_net/ui/widget/lists/model_editor.py +217 -14
  59. pygpt_net/ui/widget/lists/model_importer.py +125 -6
  60. pygpt_net/ui/widget/lists/preset.py +460 -71
  61. pygpt_net/ui/widget/lists/profile.py +149 -27
  62. pygpt_net/ui/widget/lists/uploaded.py +230 -38
  63. pygpt_net/ui/widget/option/combo.py +1046 -32
  64. pygpt_net/ui/widget/option/dictionary.py +35 -7
  65. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/METADATA +14 -57
  66. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/RECORD +69 -69
  67. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/LICENSE +0 -0
  68. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/WHEEL +0 -0
  69. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/entry_points.txt +0 -0
pygpt_net/CHANGELOG.txt CHANGED
@@ -1,3 +1,15 @@
1
+ 2.7.0 (2025-12-28)
2
+
3
+ - Added multi-select functionality using CTRL or SHIFT and batch actions to the context list, preset list, attachments list, and other list-based widgets.
4
+ - Added a search field to comboboxes, such as the model selector.
5
+ - Added a Duplicate option to the models editor.
6
+ - Added drag-and-drop to context list.
7
+ - Added multi-select, drag-and-drop, Cut, Copy, and Paste features to the File Explorer.
8
+ - Fix: scroll restoration after actions in the context list.
9
+ - Fix: 'Use as image' option in the File Explorer.
10
+ - Fix: current preset system prompt disappearing on profile change.
11
+ - Other UI fixes/improvements.
12
+
1
13
  2.6.67 (2025-12-26)
2
14
 
3
15
  - Added a provider filter to the models editor.
pygpt_net/__init__.py CHANGED
@@ -6,15 +6,15 @@
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.26 00:00:00 #
9
+ # Updated Date: 2025.12.28 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  __author__ = "Marcin Szczygliński"
13
13
  __copyright__ = "Copyright 2025, Marcin Szczygliński"
14
14
  __credits__ = ["Marcin Szczygliński"]
15
15
  __license__ = "MIT"
16
- __version__ = "2.6.67"
17
- __build__ = "2025-12-26"
16
+ __version__ = "2.7.0"
17
+ __build__ = "2025-12-28"
18
18
  __maintainer__ = "Marcin Szczygliński"
19
19
  __github__ = "https://github.com/szczyglis-dev/py-gpt"
20
20
  __report__ = "https://github.com/szczyglis-dev/py-gpt/issues"
@@ -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.23 15:00:00 #
9
+ # Updated Date: 2025.12.28 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Optional
@@ -23,6 +23,7 @@ from pygpt_net.core.text.utils import has_unclosed_code_tag
23
23
  from pygpt_net.utils import trans
24
24
  from pygpt_net.item.ctx import CtxItem
25
25
  from pygpt_net.core.events import RenderEvent
26
+ from pygpt_net.core.types import MODE_ASSISTANT
26
27
 
27
28
 
28
29
  class Assistant:
@@ -193,6 +194,9 @@ class Assistant:
193
194
 
194
195
  :param no_scroll: True if do not scroll to selected item
195
196
  """
197
+ mode = self.window.core.config.get('mode')
198
+ if mode != MODE_ASSISTANT:
199
+ return
196
200
  assistant_id = self.window.core.config.get('assistant')
197
201
  items = self.window.core.assistants.get_all()
198
202
  if assistant_id in items:
@@ -208,15 +212,16 @@ class Assistant:
208
212
 
209
213
  def select_default(self):
210
214
  """Set default assistant"""
215
+ mode = self.window.core.config.get('mode')
216
+ if mode != MODE_ASSISTANT:
217
+ return
211
218
  assistant = self.window.core.config.get('assistant')
212
219
  if assistant is None or assistant == "":
213
- mode = self.window.core.config.get('mode')
214
- if mode == 'assistant':
215
- self.window.core.config.set(
216
- 'assistant',
217
- self.window.core.assistants.get_default_assistant(),
218
- )
219
- self.update()
220
+ self.window.core.config.set(
221
+ 'assistant',
222
+ self.window.core.assistants.get_default_assistant(),
223
+ )
224
+ self.update()
220
225
 
221
226
  def create(self) -> AssistantItem:
222
227
  """
@@ -6,10 +6,10 @@
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 22:00:00 #
9
+ # Updated Date: 2025.12.27 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from typing import Optional, Any
12
+ from typing import Optional, Any, Union
13
13
 
14
14
  from PySide6.QtCore import QTimer
15
15
  from PySide6.QtWidgets import QFileDialog, QApplication
@@ -143,17 +143,21 @@ class Batch:
143
143
  QApplication.processEvents()
144
144
  self.window.core.api.openai.assistants.importer.truncate_files() # remove all files from API
145
145
 
146
- def truncate_store_files_by_idx(self, idx: int, force: bool = False):
146
+ def truncate_store_files_by_idx(self, idx: Union[int, list], force: bool = False):
147
147
  """
148
148
  Truncate all files in API (store)
149
149
 
150
- :param idx: store index
150
+ :param idx: store index or list of indexes
151
151
  :param force: if True, imports without confirmation
152
152
  """
153
- store_id = self.window.controller.assistant.store.get_by_tab_idx(idx)
154
- self.truncate_store_files(store_id, force)
153
+ store_ids = []
154
+ ids = idx if isinstance(idx, list) else [idx]
155
+ for i in ids:
156
+ store_id = self.window.controller.assistant.store.get_by_tab_idx(i)
157
+ store_ids.append(store_id)
158
+ self.truncate_store_files(store_ids, force)
155
159
 
156
- def truncate_store_files(self, store_id: str, force: bool = False):
160
+ def truncate_store_files(self, store_id: Union[str, list], force: bool = False):
157
161
  """
158
162
  Truncate all files in API (store)
159
163
 
@@ -174,31 +178,37 @@ class Batch:
174
178
  # run asynchronous
175
179
  self.window.update_status("Removing files...please wait...")
176
180
  QApplication.processEvents()
177
- self.window.core.api.openai.assistants.importer.truncate_files(store_id) # remove all files from API
181
+ ids = store_id if isinstance(store_id, list) else [store_id]
182
+ for store_id in ids:
183
+ self.window.core.api.openai.assistants.importer.truncate_files(store_id) # remove all files from API
178
184
 
179
185
  def clear_store_files_by_idx(
180
186
  self,
181
- idx: int,
187
+ idx: Union[int, list],
182
188
  force: bool = False
183
189
  ):
184
190
  """
185
191
  Clear files (store, local only)
186
192
 
187
- :param idx: store index
193
+ :param idx: store index or list of indexes
188
194
  :param force: if True, clears without confirmation
189
195
  """
190
- store_id = self.window.controller.assistant.store.get_by_tab_idx(idx)
191
- self.clear_store_files(store_id, force)
196
+ store_ids = []
197
+ ids = idx if isinstance(idx, list) else [idx]
198
+ for i in ids:
199
+ store_id = self.window.controller.assistant.store.get_by_tab_idx(i)
200
+ store_ids.append(store_id)
201
+ self.clear_store_files(store_ids, force)
192
202
 
193
203
  def clear_store_files(
194
204
  self,
195
- store_id: Optional[str] = None,
205
+ store_id: Optional[Union[str, list]] = None,
196
206
  force: bool = False
197
207
  ):
198
208
  """
199
209
  Clear files (store, local only)
200
210
 
201
- :param store_id: store ID
211
+ :param store_id: store ID or list of store IDs
202
212
  :param force: if True, clears without confirmation
203
213
  """
204
214
  if store_id is None:
@@ -214,7 +224,11 @@ class Batch:
214
224
  return
215
225
  self.window.update_status("Clearing store files...please wait...")
216
226
  QApplication.processEvents()
217
- self.window.core.assistants.files.truncate_local(store_id) # clear files local
227
+
228
+ ids = store_id if isinstance(store_id, list) else [store_id]
229
+ for store_id in ids:
230
+ self.window.core.assistants.files.truncate_local(store_id) # clear files local
231
+
218
232
  self.window.controller.assistant.files.update()
219
233
  self.window.update_status("OK. All store files cleared.")
220
234
  self.window.ui.dialogs.alert(trans("status.finished"))
@@ -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.08.24 23:00:00 #
9
+ # Updated Date: 2025.12.27 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
13
- from typing import Optional, Dict, List
13
+ from typing import Optional, Dict, List, Union
14
14
 
15
15
  from PySide6.QtWidgets import QApplication
16
16
 
@@ -78,11 +78,11 @@ class Files:
78
78
  self.window.update_status("Importing files...please wait...")
79
79
  self.window.core.api.openai.assistants.importer.import_files(store_id)
80
80
 
81
- def download(self, idx: int):
81
+ def download(self, idx: Union[int, list]):
82
82
  """
83
83
  Download file
84
84
 
85
- :param idx: selected attachment index
85
+ :param idx: selected attachment index or list of indexes
86
86
  """
87
87
  id = self.window.core.config.get('assistant')
88
88
  if id is None or id == "":
@@ -93,10 +93,11 @@ class Files:
93
93
 
94
94
  # get file by list index
95
95
  thread_id = self.window.core.config.get('assistant_thread')
96
- file_id = self.window.core.assistants.files.get_file_id_by_idx(idx, assistant.vector_store, thread_id)
97
96
 
98
- # download file
99
- self.window.controller.attachment.download(file_id)
97
+ ids = idx if isinstance(idx, list) else [idx]
98
+ for idx in ids:
99
+ file_id = self.window.core.assistants.files.get_file_id_by_idx(idx, assistant.vector_store, thread_id)
100
+ self.window.controller.attachment.download(file_id) # download file
100
101
 
101
102
  def rename(self, idx: int):
102
103
  """
@@ -186,11 +187,11 @@ class Files:
186
187
 
187
188
  self.update()
188
189
 
189
- def delete(self, idx: int, force: bool = False):
190
+ def delete(self, idx: Union[int, list], force: bool = False):
190
191
  """
191
192
  Delete file
192
193
 
193
- :param idx: file idx
194
+ :param idx: file idx or list of idxs
194
195
  :param force: force delete without confirmation
195
196
  """
196
197
  if not force:
@@ -209,17 +210,21 @@ class Files:
209
210
  if assistant is None:
210
211
  return
211
212
 
212
- # get file by list index
213
+ files = []
213
214
  thread_id = self.window.core.config.get('assistant_thread')
214
- file = self.window.core.assistants.files.get_file_by_idx(idx, assistant.vector_store, thread_id)
215
- if file is None:
216
- return
215
+ # get files by list index
216
+ ids = idx if isinstance(idx, list) else [idx]
217
+ for idx in ids:
218
+ file = self.window.core.assistants.files.get_file_by_idx(idx, assistant.vector_store, thread_id)
219
+ if file is None:
220
+ continue
221
+ files.append(file)
217
222
 
218
223
  # delete file in API
219
224
  self.window.update_status(trans('status.sending'))
220
225
  QApplication.processEvents()
221
226
  try:
222
- self.window.core.assistants.files.delete(file) # delete from DB, API and vector stores
227
+ self.window.core.assistants.files.delete(files) # delete from DB, API and vector stores
223
228
 
224
229
  # update store status
225
230
  if assistant.vector_store:
@@ -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.02 22:00:00 #
9
+ # Updated Date: 2025.12.27 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
13
13
  import json
14
- from typing import Optional
14
+ from typing import Optional, Union
15
15
 
16
16
  from PySide6.QtWidgets import QApplication
17
17
  from PySide6.QtGui import QStandardItem
@@ -183,32 +183,44 @@ class VectorStore:
183
183
  if update and store.id == self.current:
184
184
  self.update_current()
185
185
 
186
- def refresh_by_idx(self, idx: int):
186
+ def refresh_by_idx(self, idx: Union[int, list]):
187
187
  """
188
188
  Refresh store by idx
189
189
 
190
- :param idx: store idx
190
+ :param idx: store idx or list of idxs
191
191
  """
192
- store_id = self.get_by_tab_idx(idx)
193
- if store_id is not None:
194
- self.refresh_by_store_id(store_id)
192
+ store_ids = []
193
+ ids = idx if isinstance(idx, list) else [idx]
194
+ for i in ids:
195
+ store_id = self.get_by_tab_idx(i)
196
+ if store_id is not None:
197
+ store_ids.append(store_id)
198
+ self.refresh_by_store_id(store_ids)
195
199
 
196
- def refresh_by_store_id(self, store_id: str):
200
+ def refresh_by_store_id(self, store_id: Union[str, list]):
197
201
  """
198
202
  Refresh store by ID
199
203
 
200
204
  :param store_id: store id
201
205
  """
202
- if store_id is not None and store_id in self.window.core.assistants.store.items:
203
- store = self.window.core.assistants.store.items[store_id]
204
- if store is not None:
205
- self.window.update_status(trans('status.sending'))
206
- QApplication.processEvents()
207
- self.refresh_store(store)
208
- self.window.update_status(trans('status.assistant.saved'))
209
- self.update()
210
- if self.current == store_id:
211
- self.update_files_list()
206
+ ids = store_id if isinstance(store_id, list) else [store_id]
207
+ updated = False
208
+ is_current = False
209
+ for store_id in ids:
210
+ if store_id is not None and store_id in self.window.core.assistants.store.items:
211
+ store = self.window.core.assistants.store.items[store_id]
212
+ if store is not None:
213
+ self.window.update_status(trans('status.sending'))
214
+ QApplication.processEvents()
215
+ self.refresh_store(store)
216
+ updated = True
217
+ if self.current == store_id:
218
+ is_current = True
219
+ if updated:
220
+ self.window.update_status(trans('status.assistant.saved'))
221
+ self.update()
222
+ if is_current:
223
+ self.update_files_list()
212
224
 
213
225
  def update_current(self):
214
226
  """Update current store"""
@@ -327,27 +339,32 @@ class VectorStore:
327
339
 
328
340
  def delete_by_idx(
329
341
  self,
330
- idx: int,
342
+ idx: Union[int, list],
331
343
  force: bool = False
332
344
  ):
333
345
  """
334
346
  Delete store by idx
335
347
 
336
- :param idx: store idx
348
+ :param idx: store idx or list of idxs
337
349
  :param force: force delete
338
350
  """
339
- store_id = self.get_by_tab_idx(idx)
340
- self.delete(store_id, force=force)
351
+ store_ids = []
352
+ ids = idx if isinstance(idx, list) else [idx]
353
+ for i in ids:
354
+ store_id = self.get_by_tab_idx(i)
355
+ if store_id is not None:
356
+ store_ids.append(store_id)
357
+ self.delete(store_ids, force=force)
341
358
 
342
359
  def delete(
343
360
  self,
344
- store_id: Optional[str] = None,
361
+ store_id: Optional[Union[str, list]] = None,
345
362
  force: bool = False
346
363
  ):
347
364
  """
348
365
  Delete store by idx
349
366
 
350
- :param store_id: store id
367
+ :param store_id: store id or list of store ids
351
368
  :param force: force delete
352
369
  """
353
370
  if not force:
@@ -363,25 +380,30 @@ class VectorStore:
363
380
  return
364
381
 
365
382
  self.window.update_status(trans('status.sending'))
383
+ updated = False
366
384
  QApplication.processEvents()
367
- if self.current == store_id:
368
- self.current = None
369
- try:
370
- print("Deleting store: {}".format(store_id))
371
- if self.window.core.assistants.store.delete(store_id):
372
- self.window.controller.assistant.batch.remove_store_from_assistants(store_id)
373
- self.window.update_status(trans('status.deleted'))
374
- self.window.core.assistants.store.save()
375
- self.window.controller.assistant.files.update()
376
- self.update() # update stores list in assistant dialog
377
- self.init()
378
- self.restore_selection()
379
- self.update_files_list()
380
- else:
385
+ ids = store_id if isinstance(store_id, list) else [store_id]
386
+ for store_id in ids:
387
+ if self.current == store_id:
388
+ self.current = None
389
+ try:
390
+ print("Deleting store: {}".format(store_id))
391
+ if self.window.core.assistants.store.delete(store_id):
392
+ self.window.controller.assistant.batch.remove_store_from_assistants(store_id)
393
+ self.window.update_status(trans('status.deleted'))
394
+ self.window.core.assistants.store.save()
395
+ updated = True
396
+ else:
397
+ self.window.update_status(trans('status.error'))
398
+ except Exception as e:
381
399
  self.window.update_status(trans('status.error'))
382
- except Exception as e:
383
- self.window.update_status(trans('status.error'))
384
- self.window.ui.dialogs.alert(e)
400
+ self.window.ui.dialogs.alert(e)
401
+ if updated:
402
+ self.window.controller.assistant.files.update()
403
+ self.update() # update stores list in assistant dialog
404
+ self.init()
405
+ self.restore_selection()
406
+ self.update_files_list()
385
407
 
386
408
  def set_by_tab(self, idx: int):
387
409
  """
@@ -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.08.28 09:00:00 #
9
+ # Updated Date: 2025.12.27 21:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
13
13
  from datetime import datetime
14
- from typing import Optional
14
+ from typing import Optional, Union
15
15
  from urllib.parse import urlparse
16
16
 
17
17
  from PySide6.QtGui import QImage
@@ -128,14 +128,14 @@ class Attachment:
128
128
 
129
129
  def delete(
130
130
  self,
131
- idx: int,
131
+ idx: Union[int, list],
132
132
  force: bool = False,
133
133
  remove_local: bool = False
134
134
  ):
135
135
  """
136
136
  Delete attachment
137
137
 
138
- :param idx: index of attachment
138
+ :param idx: index of attachment to delete (or list of indices)
139
139
  :param force: force delete
140
140
  :param remove_local: remove local file
141
141
  """
@@ -148,19 +148,25 @@ class Attachment:
148
148
  )
149
149
  return
150
150
 
151
- file_id = self.window.core.attachments.get_id_by_idx(
152
- mode=mode,
153
- idx=idx,
154
- )
155
- self.window.core.attachments.delete(
156
- mode=mode,
157
- id=file_id,
158
- remove_local=remove_local,
159
- )
151
+ ids = idx if isinstance(idx, list) else [idx]
152
+ file_ids = []
153
+ for idx in ids:
154
+ file_id = self.window.core.attachments.get_id_by_idx(
155
+ mode=mode,
156
+ idx=idx,
157
+ )
158
+ file_ids.append(file_id)
160
159
 
161
- # clear current if current == deleted
162
- if self.window.core.attachments.current == file_id:
163
- self.window.core.attachments.current = None
160
+ for file_id in file_ids:
161
+ self.window.core.attachments.delete(
162
+ mode=mode,
163
+ id=file_id,
164
+ remove_local=remove_local,
165
+ )
166
+
167
+ # clear current if current == deleted
168
+ if self.window.core.attachments.current == file_id:
169
+ self.window.core.attachments.current = None
164
170
 
165
171
  if not self.has(mode):
166
172
  self.window.controller.chat.vision.unavailable()
@@ -363,38 +369,42 @@ class Attachment:
363
369
  self.update()
364
370
  self.window.ui.dialog['url'].close()
365
371
 
366
- def open_dir(self, mode: str, idx: int):
372
+ def open_dir(self, mode: str, idx: Union[int, list]):
367
373
  """
368
374
  Open in directory
369
375
 
370
376
  :param mode: mode
371
- :param idx: index
377
+ :param idx: index or list of indices
372
378
  """
373
- path = self.get_path_by_idx(
374
- mode=mode,
375
- idx=idx,
376
- )
377
- if path is not None and path != '' and os.path.exists(path):
378
- self.window.controller.files.open_dir(
379
- path=path,
380
- select=True,
379
+ ids = idx if isinstance(idx, list) else [idx]
380
+ for idx in ids:
381
+ path = self.get_path_by_idx(
382
+ mode=mode,
383
+ idx=idx,
381
384
  )
385
+ if path is not None and path != '' and os.path.exists(path):
386
+ self.window.controller.files.open_dir(
387
+ path=path,
388
+ select=True,
389
+ )
382
390
 
383
- def open(self, mode: str, idx: int):
391
+ def open(self, mode: str, idx: Union[int, list]):
384
392
  """
385
393
  Open attachment
386
394
 
387
395
  :param mode: mode
388
- :param idx: index
396
+ :param idx: index or list of indices
389
397
  """
390
- path = self.get_path_by_idx(
391
- mode=mode,
392
- idx=idx,
393
- )
394
- if path is not None and path != '' and os.path.exists(path):
395
- self.window.controller.files.open(
396
- path=path,
398
+ ids = idx if isinstance(idx, list) else [idx]
399
+ for idx in ids:
400
+ path = self.get_path_by_idx(
401
+ mode=mode,
402
+ idx=idx,
397
403
  )
404
+ if path is not None and path != '' and os.path.exists(path):
405
+ self.window.controller.files.open(
406
+ path=path,
407
+ )
398
408
 
399
409
  def get_path_by_idx(self, mode: str, idx: int) -> str:
400
410
  """