pygpt-net 2.7.8__py3-none-any.whl → 2.7.10__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 (112) hide show
  1. pygpt_net/CHANGELOG.txt +14 -0
  2. pygpt_net/LICENSE +1 -1
  3. pygpt_net/__init__.py +3 -3
  4. pygpt_net/config.py +15 -1
  5. pygpt_net/controller/chat/common.py +5 -4
  6. pygpt_net/controller/chat/image.py +3 -3
  7. pygpt_net/controller/chat/stream.py +76 -41
  8. pygpt_net/controller/chat/stream_worker.py +3 -3
  9. pygpt_net/controller/ctx/extra.py +3 -1
  10. pygpt_net/controller/dialogs/debug.py +37 -8
  11. pygpt_net/controller/kernel/kernel.py +3 -7
  12. pygpt_net/controller/lang/custom.py +25 -12
  13. pygpt_net/controller/lang/lang.py +45 -3
  14. pygpt_net/controller/lang/mapping.py +15 -2
  15. pygpt_net/controller/notepad/notepad.py +68 -25
  16. pygpt_net/controller/presets/editor.py +5 -1
  17. pygpt_net/controller/presets/presets.py +17 -5
  18. pygpt_net/controller/realtime/realtime.py +13 -1
  19. pygpt_net/controller/theme/theme.py +11 -2
  20. pygpt_net/controller/ui/tabs.py +1 -1
  21. pygpt_net/core/ctx/output.py +38 -12
  22. pygpt_net/core/db/database.py +4 -2
  23. pygpt_net/core/debug/console/console.py +30 -2
  24. pygpt_net/core/debug/context.py +2 -1
  25. pygpt_net/core/debug/ui.py +26 -4
  26. pygpt_net/core/filesystem/filesystem.py +6 -2
  27. pygpt_net/core/notepad/notepad.py +2 -2
  28. pygpt_net/core/tabs/tabs.py +79 -19
  29. pygpt_net/data/config/config.json +4 -3
  30. pygpt_net/data/config/models.json +37 -22
  31. pygpt_net/data/config/settings.json +12 -0
  32. pygpt_net/data/locale/locale.ar.ini +1833 -0
  33. pygpt_net/data/locale/locale.bg.ini +1833 -0
  34. pygpt_net/data/locale/locale.cs.ini +1833 -0
  35. pygpt_net/data/locale/locale.da.ini +1833 -0
  36. pygpt_net/data/locale/locale.de.ini +4 -1
  37. pygpt_net/data/locale/locale.en.ini +70 -67
  38. pygpt_net/data/locale/locale.es.ini +4 -1
  39. pygpt_net/data/locale/locale.fi.ini +1833 -0
  40. pygpt_net/data/locale/locale.fr.ini +4 -1
  41. pygpt_net/data/locale/locale.he.ini +1833 -0
  42. pygpt_net/data/locale/locale.hi.ini +1833 -0
  43. pygpt_net/data/locale/locale.hu.ini +1833 -0
  44. pygpt_net/data/locale/locale.it.ini +4 -1
  45. pygpt_net/data/locale/locale.ja.ini +1833 -0
  46. pygpt_net/data/locale/locale.ko.ini +1833 -0
  47. pygpt_net/data/locale/locale.nl.ini +1833 -0
  48. pygpt_net/data/locale/locale.no.ini +1833 -0
  49. pygpt_net/data/locale/locale.pl.ini +5 -2
  50. pygpt_net/data/locale/locale.pt.ini +1833 -0
  51. pygpt_net/data/locale/locale.ro.ini +1833 -0
  52. pygpt_net/data/locale/locale.ru.ini +1833 -0
  53. pygpt_net/data/locale/locale.sk.ini +1833 -0
  54. pygpt_net/data/locale/locale.sv.ini +1833 -0
  55. pygpt_net/data/locale/locale.tr.ini +1833 -0
  56. pygpt_net/data/locale/locale.uk.ini +4 -1
  57. pygpt_net/data/locale/locale.zh.ini +4 -1
  58. pygpt_net/item/notepad.py +8 -2
  59. pygpt_net/migrations/Version20260121190000.py +25 -0
  60. pygpt_net/migrations/Version20260122140000.py +25 -0
  61. pygpt_net/migrations/__init__.py +5 -1
  62. pygpt_net/preload.py +246 -3
  63. pygpt_net/provider/api/__init__.py +16 -2
  64. pygpt_net/provider/api/anthropic/__init__.py +21 -7
  65. pygpt_net/provider/api/google/__init__.py +21 -7
  66. pygpt_net/provider/api/google/image.py +89 -2
  67. pygpt_net/provider/api/google/realtime/client.py +70 -24
  68. pygpt_net/provider/api/google/realtime/realtime.py +48 -12
  69. pygpt_net/provider/api/google/video.py +2 -2
  70. pygpt_net/provider/api/openai/__init__.py +26 -11
  71. pygpt_net/provider/api/openai/image.py +79 -3
  72. pygpt_net/provider/api/openai/realtime/realtime.py +26 -6
  73. pygpt_net/provider/api/openai/responses.py +11 -31
  74. pygpt_net/provider/api/openai/video.py +2 -2
  75. pygpt_net/provider/api/x_ai/__init__.py +21 -10
  76. pygpt_net/provider/api/x_ai/realtime/client.py +185 -146
  77. pygpt_net/provider/api/x_ai/realtime/realtime.py +30 -15
  78. pygpt_net/provider/api/x_ai/remote_tools.py +83 -0
  79. pygpt_net/provider/api/x_ai/tools.py +51 -0
  80. pygpt_net/provider/core/config/patch.py +12 -1
  81. pygpt_net/provider/core/model/patch.py +36 -1
  82. pygpt_net/provider/core/notepad/db_sqlite/storage.py +53 -10
  83. pygpt_net/tools/agent_builder/ui/dialogs.py +2 -1
  84. pygpt_net/tools/audio_transcriber/ui/dialogs.py +2 -1
  85. pygpt_net/tools/code_interpreter/ui/dialogs.py +2 -1
  86. pygpt_net/tools/html_canvas/ui/dialogs.py +2 -1
  87. pygpt_net/tools/image_viewer/ui/dialogs.py +3 -5
  88. pygpt_net/tools/indexer/ui/dialogs.py +2 -1
  89. pygpt_net/tools/media_player/ui/dialogs.py +2 -1
  90. pygpt_net/tools/translator/ui/dialogs.py +2 -1
  91. pygpt_net/tools/translator/ui/widgets.py +6 -2
  92. pygpt_net/ui/dialog/about.py +2 -2
  93. pygpt_net/ui/dialog/db.py +2 -1
  94. pygpt_net/ui/dialog/debug.py +169 -6
  95. pygpt_net/ui/dialog/logger.py +6 -2
  96. pygpt_net/ui/dialog/models.py +36 -3
  97. pygpt_net/ui/dialog/preset.py +5 -1
  98. pygpt_net/ui/dialog/remote_store.py +2 -1
  99. pygpt_net/ui/main.py +3 -2
  100. pygpt_net/ui/widget/dialog/editor_file.py +2 -1
  101. pygpt_net/ui/widget/lists/debug.py +12 -7
  102. pygpt_net/ui/widget/option/checkbox.py +2 -8
  103. pygpt_net/ui/widget/option/combo.py +10 -2
  104. pygpt_net/ui/widget/textarea/console.py +156 -7
  105. pygpt_net/ui/widget/textarea/highlight.py +66 -0
  106. pygpt_net/ui/widget/textarea/input.py +624 -57
  107. pygpt_net/ui/widget/textarea/notepad.py +294 -27
  108. {pygpt_net-2.7.8.dist-info → pygpt_net-2.7.10.dist-info}/LICENSE +1 -1
  109. {pygpt_net-2.7.8.dist-info → pygpt_net-2.7.10.dist-info}/METADATA +16 -64
  110. {pygpt_net-2.7.8.dist-info → pygpt_net-2.7.10.dist-info}/RECORD +112 -91
  111. {pygpt_net-2.7.8.dist-info → pygpt_net-2.7.10.dist-info}/WHEEL +0 -0
  112. {pygpt_net-2.7.8.dist-info → pygpt_net-2.7.10.dist-info}/entry_points.txt +0 -0
@@ -6,17 +6,25 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2026.01.03 00:00:00 #
9
+ # Updated Date: 2026.01.22 16:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import Qt, QEvent, QTimer
13
- from PySide6.QtGui import QAction, QIcon, QKeySequence, QTextCursor, QFontMetrics
13
+ from PySide6.QtGui import (
14
+ QAction,
15
+ QIcon,
16
+ QKeySequence,
17
+ QTextCursor,
18
+ QFontMetrics,
19
+ QColor,
20
+ )
14
21
  from PySide6.QtWidgets import QTextEdit, QWidget, QVBoxLayout
15
22
 
16
23
  from pygpt_net.core.tabs.tab import Tab
17
24
  from pygpt_net.core.text.finder import Finder
18
25
  from pygpt_net.ui.widget.element.labels import HelpLabel
19
26
  from pygpt_net.utils import trans
27
+ from .highlight import MarkerHighlighter
20
28
 
21
29
 
22
30
  class NotepadWidget(QWidget):
@@ -75,6 +83,7 @@ class NotepadWidget(QWidget):
75
83
  """On destroy"""
76
84
  # unregister finder from memory
77
85
  self.window.controller.finder.unset(self.textarea.finder)
86
+
78
87
  def on_delete(self):
79
88
  """On delete"""
80
89
  self.tab = None # clear tab reference
@@ -85,6 +94,8 @@ class NotepadOutput(QTextEdit):
85
94
  ICON_VOLUME = QIcon(":/icons/volume.svg")
86
95
  ICON_SAVE = QIcon(":/icons/save.svg")
87
96
  ICON_SEARCH = QIcon(":/icons/search.svg")
97
+ ICON_MARK = QIcon(":/icons/edit.svg")
98
+ ICON_UNMARK = QIcon(":/icons/close.svg")
88
99
 
89
100
  def __init__(self, window=None):
90
101
  """
@@ -106,6 +117,7 @@ class NotepadOutput(QTextEdit):
106
117
  self.last_scroll_pos = None
107
118
  self.installEventFilter(self)
108
119
  self.setProperty('class', 'layout-notepad')
120
+ self.initialized = False
109
121
 
110
122
  metrics = QFontMetrics(self.font())
111
123
  space_width = metrics.horizontalAdvance(" ")
@@ -114,20 +126,47 @@ class NotepadOutput(QTextEdit):
114
126
  self._vscroll = self.verticalScrollBar()
115
127
  self._vscroll.valueChanged.connect(self._on_scrollbar_value_changed)
116
128
  self._restore_attempts = 0
129
+ self._pending_scroll_pos = None
130
+
131
+ # highlight state (using QSyntaxHighlighter for rendering)
132
+ self._highlights = [] # list of (start, length)
133
+
134
+ # timers/slots must be available even if later connections fail
135
+ self._save_timer = QTimer(self)
136
+ self._save_timer.setSingleShot(True)
137
+ self._save_timer.setInterval(400)
138
+ self._save_timer.timeout.connect(self._persist)
139
+
140
+ # highlighter
141
+ self._highlighter = MarkerHighlighter(self.document(), self.get_highlights, self.get_highlight_color)
117
142
 
118
143
  def on_delete(self):
119
144
  """On delete"""
120
145
  if self.finder:
121
146
  self.finder.disconnect() # disconnect finder
122
147
  self.finder = None # delete finder
148
+ if self._save_timer.isActive():
149
+ self._save_timer.stop()
123
150
  self.deleteLater()
124
151
 
125
152
  def showEvent(self, event):
153
+ """On show event"""
126
154
  super().showEvent(event)
127
155
  self._restore_attempts = 0
128
156
  QTimer.singleShot(0, self.restore_scroll_pos)
157
+ self.initialized = True
158
+
159
+ def changeEvent(self, event):
160
+ """React to theme/palette changes"""
161
+ if event.type() in (QEvent.PaletteChange, QEvent.ApplicationPaletteChange, QEvent.StyleChange):
162
+ try:
163
+ self._highlighter.rehighlight()
164
+ except Exception:
165
+ pass
166
+ super().changeEvent(event)
129
167
 
130
168
  def scroll_to_bottom(self):
169
+ """Scroll to bottom"""
131
170
  self.moveCursor(QTextCursor.End)
132
171
  self.ensureCursorVisible()
133
172
  scroll_bar = self._vscroll
@@ -155,26 +194,59 @@ class NotepadOutput(QTextEdit):
155
194
  self.tab = tab
156
195
 
157
196
  def setText(self, text: str):
197
+ """
198
+ Set text
199
+
200
+ :param text: Text
201
+ """
158
202
  if self.toPlainText() == text:
159
203
  return
160
204
  self.setPlainText(text)
205
+ self._highlighter.rehighlight() # refresh highlighting on new content
161
206
 
162
207
  def text_changed(self):
163
208
  """On text changed"""
164
209
  if not self.window.core.notepad.locked:
165
- self.window.controller.notepad.save(self.id) # use notepad id
166
- if self.finder is not None:
167
- self.finder.text_changed()
168
- self.last_scroll_pos = self._vscroll.value()
210
+ if self.finder is not None:
211
+ self.finder.text_changed()
212
+ self.last_scroll_pos = self._vscroll.value()
213
+ if self.initialized and not self.toPlainText():
214
+ QTimer.singleShot(0, lambda: self.clear_highlights(persist=False)) # if empty, reset highlights
215
+ self.schedule_save()
169
216
 
170
217
  def _on_scrollbar_value_changed(self, value: int):
171
- self.last_scroll_pos = value
218
+ """
219
+ On scrollbar value changed
220
+
221
+ :param value: New value
222
+ """
223
+ if not self.window.core.notepad.locked:
224
+ self.last_scroll_pos = value
225
+ self.schedule_save()
226
+
227
+ def is_initialized(self) -> bool:
228
+ """
229
+ Check if initialized
230
+
231
+ :return: True if initialized
232
+ """
233
+ return self.initialized
172
234
 
173
235
  def restore_scroll_pos(self):
236
+ """Restore last scroll position"""
237
+ if self.window.core.notepad.locked:
238
+ QTimer.singleShot(25, self.restore_scroll_pos)
239
+ return
240
+ if not self.initialized:
241
+ return
242
+ if self._pending_scroll_pos is not None:
243
+ self.last_scroll_pos = self._pending_scroll_pos
174
244
  if self.last_scroll_pos is None:
175
245
  return
176
246
  scroll_bar = self._vscroll
177
247
  current_max = scroll_bar.maximum()
248
+ if current_max == 0:
249
+ return # nothing to scroll
178
250
  if self.last_scroll_pos > current_max:
179
251
  if self._restore_attempts < 30:
180
252
  self._restore_attempts += 1
@@ -182,7 +254,35 @@ class NotepadOutput(QTextEdit):
182
254
  else:
183
255
  scroll_bar.setValue(current_max)
184
256
  else:
257
+ self.window.core.notepad.locked = True
185
258
  scroll_bar.setValue(self.last_scroll_pos)
259
+ if self._pending_scroll_pos is not None:
260
+ self._pending_scroll_pos = None
261
+ self.window.core.notepad.locked = False
262
+
263
+ def set_scroll_pos(self, pos: int):
264
+ """
265
+ Set scroll position
266
+
267
+ :param pos: Scroll position
268
+ """
269
+ self._pending_scroll_pos = pos
270
+ self.last_scroll_pos = pos
271
+
272
+ def get_scroll_pos(self) -> int:
273
+ """
274
+ Get scroll position
275
+
276
+ :return: Scroll position
277
+ """
278
+ return self._vscroll.value()
279
+
280
+ def schedule_save(self):
281
+ """Schedule save of notepad content"""
282
+ try:
283
+ self._save_timer.start()
284
+ except Exception:
285
+ pass
186
286
 
187
287
  def contextMenuEvent(self, event):
188
288
  """
@@ -193,6 +293,23 @@ class NotepadOutput(QTextEdit):
193
293
  menu = self.createStandardContextMenu()
194
294
  cursor = self.textCursor()
195
295
  selected_text = cursor.selectedText()
296
+ has_selection = bool(selected_text)
297
+
298
+ # Mark / Unmark actions for selection
299
+ if has_selection:
300
+ start = min(cursor.selectionStart(), cursor.selectionEnd())
301
+ end = max(cursor.selectionStart(), cursor.selectionEnd())
302
+ overlap = self._selection_overlaps_any_highlight(start, end)
303
+
304
+ action_mark = QAction(self.ICON_MARK, trans("action.mark"), self)
305
+ action_mark.triggered.connect(self.mark_selection)
306
+ menu.addAction(action_mark)
307
+
308
+ action_unmark = QAction(self.ICON_UNMARK, trans("action.unmark"), self)
309
+ action_unmark.setEnabled(overlap)
310
+ action_unmark.triggered.connect(self.unmark_selection)
311
+ menu.addAction(action_unmark)
312
+
196
313
  if selected_text:
197
314
  plain_text = cursor.selection().toPlainText()
198
315
 
@@ -239,6 +356,25 @@ class NotepadOutput(QTextEdit):
239
356
  """On content update"""
240
357
  self.finder.clear() # clear finder
241
358
 
359
+ def on_zoom_changed(self, value: int):
360
+ """
361
+ On font size changed
362
+
363
+ :param value: New font size
364
+ """
365
+ self.value = value
366
+ self.window.core.config.data['font_size'] = value
367
+ self.window.core.config.save()
368
+ option = self.window.controller.settings.editor.get_option('font_size')
369
+ option['value'] = self.value
370
+ self.window.controller.config.apply(
371
+ parent_id='config',
372
+ key='font_size',
373
+ option=option,
374
+ )
375
+ self.window.controller.ui.update_font_size()
376
+ self.last_scroll_pos = self._vscroll.value()
377
+
242
378
  def keyPressEvent(self, e):
243
379
  """
244
380
  Key press event
@@ -286,25 +422,6 @@ class NotepadOutput(QTextEdit):
286
422
  super(NotepadOutput, self).wheelEvent(event)
287
423
  self.last_scroll_pos = self._vscroll.value()
288
424
 
289
- def on_zoom_changed(self, value: int):
290
- """
291
- On font size changed
292
-
293
- :param value: New font size
294
- """
295
- self.value = value
296
- self.window.core.config.data['font_size'] = value
297
- self.window.core.config.save()
298
- option = self.window.controller.settings.editor.get_option('font_size')
299
- option['value'] = self.value
300
- self.window.controller.config.apply(
301
- parent_id='config',
302
- key='font_size',
303
- option=option,
304
- )
305
- self.window.controller.ui.update_font_size()
306
- self.last_scroll_pos = self._vscroll.value()
307
-
308
425
  def focusInEvent(self, e):
309
426
  """
310
427
  Focus in event
@@ -321,4 +438,154 @@ class NotepadOutput(QTextEdit):
321
438
  :param e: focus event
322
439
  """
323
440
  super(NotepadOutput, self).focusOutEvent(e)
324
- self.window.controller.finder.focus_out(self.finder)
441
+ self.window.controller.finder.focus_out(self.finder)
442
+
443
+ # ==== Marking / Highlights API ====
444
+
445
+ def mark_selection(self):
446
+ """Apply highlight to current selection"""
447
+ cursor = self.textCursor()
448
+ if not cursor.hasSelection():
449
+ return
450
+ start = min(cursor.selectionStart(), cursor.selectionEnd())
451
+ end = max(cursor.selectionStart(), cursor.selectionEnd())
452
+ self._add_highlight((start, end - start))
453
+ self._highlighter.rehighlight()
454
+ self._persist()
455
+
456
+ def unmark_selection(self):
457
+ """Remove highlight from current selection"""
458
+ cursor = self.textCursor()
459
+ if not cursor.hasSelection():
460
+ return
461
+ start = min(cursor.selectionStart(), cursor.selectionEnd())
462
+ end = max(cursor.selectionStart(), cursor.selectionEnd())
463
+ self._remove_range_from_highlights(start, end - start)
464
+ self._highlighter.rehighlight()
465
+ self._persist()
466
+
467
+ def get_highlights(self):
468
+ """Return current highlights as list of (start, length)"""
469
+ return list(self._highlights)
470
+
471
+ def set_highlights(self, highlights):
472
+ """Set highlights and repaint"""
473
+ self._highlights = self._merge_ranges(self._sanitize_ranges(highlights))
474
+ self._highlighter.rehighlight()
475
+
476
+ def clear_highlights(self, persist: bool = True):
477
+ """Clear all highlights"""
478
+ self._highlights = []
479
+ self._highlighter.rehighlight()
480
+ if persist:
481
+ self._persist()
482
+
483
+ # ==== Highlight colors / theme ====
484
+
485
+ def get_highlight_color(self):
486
+ """
487
+ Return (text_color, background_color) for highlights based on current theme.
488
+ """
489
+ is_dark = self._is_dark_theme()
490
+ if is_dark:
491
+ text_color = QColor(0, 0, 0)
492
+ bg_color = QColor(255, 255, 0) # yellow
493
+ else:
494
+ text_color = QColor(0, 0, 0)
495
+ bg_color = QColor(255, 255, 0) # yellow
496
+ return text_color, bg_color
497
+
498
+ def apply_highlight_theme(self):
499
+ """
500
+ Public method to refresh highlight colors. Can be called by theme controller
501
+ after theme switch.
502
+ """
503
+ try:
504
+ self._highlighter.rehighlight()
505
+ except Exception:
506
+ pass
507
+
508
+ def _is_dark_theme(self) -> bool:
509
+ """
510
+ Get whether current theme is dark
511
+ """
512
+ return self.window.controller.theme.is_dark_theme()
513
+
514
+ # ==== Internal highlight helpers ====
515
+
516
+ def _persist(self):
517
+ """Persist notepad state"""
518
+ if self._save_timer.isActive():
519
+ self._save_timer.stop()
520
+ try:
521
+ self.window.controller.notepad.save(self.id) # save content + marking
522
+ except Exception as e:
523
+ print(e)
524
+
525
+ def _sanitize_ranges(self, ranges):
526
+ """Sanitize ranges to (start>=0, length>0) integers"""
527
+ out = []
528
+ for r in ranges or []:
529
+ try:
530
+ s = int(r[0])
531
+ l = int(r[1])
532
+ except Exception:
533
+ continue
534
+ if s < 0 or l <= 0:
535
+ continue
536
+ out.append((s, l))
537
+ out.sort(key=lambda x: x[0])
538
+ return out
539
+
540
+ def _merge_ranges(self, ranges):
541
+ """Merge overlapping/adjacent ranges"""
542
+ if not ranges:
543
+ return []
544
+ merged = []
545
+ for s, l in ranges:
546
+ if not merged:
547
+ merged.append([s, s + l])
548
+ continue
549
+ ps, pe = merged[-1]
550
+ se = s + l
551
+ if s <= pe:
552
+ merged[-1][1] = max(pe, se)
553
+ else:
554
+ merged.append([s, se])
555
+ return [(s, e - s) for s, e in merged]
556
+
557
+ def _add_highlight(self, rng):
558
+ """Add a highlight range and merge"""
559
+ s, l = int(rng[0]), int(rng[1])
560
+ if l <= 0:
561
+ return
562
+ self._highlights.append((s, l))
563
+ self._highlights = self._merge_ranges(self._sanitize_ranges(self._highlights))
564
+ self.schedule_save()
565
+
566
+ def _remove_range_from_highlights(self, s, l):
567
+ """Subtract a range from all highlights"""
568
+ if l <= 0:
569
+ return
570
+ start = s
571
+ end = s + l
572
+ result = []
573
+ for hs, hl in self._highlights:
574
+ he = hs + hl
575
+ if he <= start or hs >= end:
576
+ result.append((hs, hl))
577
+ continue
578
+ if hs < start:
579
+ result.append((hs, start - hs))
580
+ if he > end:
581
+ result.append((end, he - end))
582
+ self._highlights = self._merge_ranges(self._sanitize_ranges(result))
583
+ self.schedule_save()
584
+
585
+ def _selection_overlaps_any_highlight(self, s, e):
586
+ """Check if selection overlaps any highlight"""
587
+ for hs, hl in self._highlights:
588
+ he = hs + hl
589
+ if not (he <= s or hs >= e):
590
+ return True
591
+ return False
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Marcin Szczygliński
3
+ Copyright (c) 2026 Marcin Szczygliński
4
4
 
5
5
  GitHub: https://github.com/szczyglis-dev/py-gpt
6
6
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pygpt-net
3
- Version: 2.7.8
3
+ Version: 2.7.10
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, MCP, 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
@@ -120,7 +120,7 @@ Description-Content-Type: text/markdown
120
120
 
121
121
  [![pygpt](https://snapcraft.io/pygpt/badge.svg)](https://snapcraft.io/pygpt)
122
122
 
123
- Release: **2.7.8** | build: **2026-01-06** | Python: **>=3.10, <3.14**
123
+ Release: **2.7.10** | build: **2026-02-03** | Python: **>=3.10, <3.14**
124
124
 
125
125
  > Official website: https://pygpt.net | Documentation: https://pygpt.readthedocs.io
126
126
  >
@@ -3796,6 +3796,20 @@ may consume additional tokens that are not displayed in the main window.
3796
3796
 
3797
3797
  ## Recent changes:
3798
3798
 
3799
+ **2.7.10 (2026-02-03)**
3800
+
3801
+ - Fixed an issue where an avatar could be overwritten when creating a new preset.
3802
+ - Fixed an issue where a new context was not created when opening a new tab in the second column.
3803
+ - Added prompt history navigation to the input field (Ctrl + Up/Down arrow keys).
3804
+ - Added initial image centering when loading the Image Viewer.
3805
+ - Added a Mark/Unmark feature to the Notepad widget.
3806
+ - Added 18 new languages: Arabic (ar), Bulgarian (bg), Czech (cs), Danish (da), Finnish (fi), Hebrew (he), Hindi (hi), Hungarian (hu), Japanese (ja), Korean (ko), Dutch (nl), Norwegian (no), Portuguese (pt), Romanian (ro), Russian (ru), Slovak (sk), Swedish (sv), Turkish (tr).
3807
+
3808
+ **2.7.9 (2026-01-08)**
3809
+
3810
+ - Improved realtime audio mode.
3811
+ - Added xAI provider and Grok support in realtime audio mode.
3812
+
3799
3813
  **2.7.8 (2026-01-06)**
3800
3814
 
3801
3815
  - Added the xAI Collections remote tool and integrated collections management into the Remote Vector Stores tool.
@@ -3817,68 +3831,6 @@ may consume additional tokens that are not displayed in the main window.
3817
3831
  - Added a zoom menu to textarea and web widgets.
3818
3832
  - Added the ability to close tabs with a middle mouse button click.
3819
3833
 
3820
- **2.7.5 (2026-01-03)**
3821
-
3822
- - Added Sandbox/Playwright option to Computer Use mode.
3823
- - Added support for Google models in Computer Use mode and introduced a new model: gemini-2.5-computer-use-preview-10-2025.
3824
- - Added support for Google models in Research mode and introduced a new model: deep-research-pro-preview-12-2025.
3825
- - Added the Google Vector Stores tool.
3826
-
3827
- **2.7.4 (2025-12-31)**
3828
-
3829
- - Added a splash screen.
3830
- - Added Preview and Download links to the Image and Video outputs.
3831
- - Added Negative prompt input to Image and Video mode.
3832
- - Improved focus handling.
3833
- - UI improvements.
3834
-
3835
- **2.7.3 (2025-12-30)**
3836
-
3837
- - Added the `Remix/Extend` option in Image and Video generation mode. This allows the use of a previously generated image or video as a reference. It can be used for adding or changing elements in a previously generated image or video instead of creating a new one from scratch. See the docs: `Modes -> Image and Video generation -> Remix, Edit, or Extend`.
3838
-
3839
- **2.7.2 (2025-12-29)**
3840
-
3841
- - Fixed: non-searchable combobox width.
3842
- - Improved updater.
3843
- - Added .AppImage build.
3844
-
3845
- **2.7.1 (2025-12-28)**
3846
-
3847
- - Improved UI elements.
3848
- - Optimized Painter rendering and redraw functions.
3849
- - Added Pack/Unpack feature to File Explorer.
3850
- - Fixed: image restoration in Painter.
3851
- - Fixed: tab title updating upon context deletion.
3852
-
3853
- **2.7.0 (2025-12-28)**
3854
-
3855
- - Added multi-select functionality using CTRL or SHIFT and batch actions to the context list, preset list, attachments list, and other list-based widgets.
3856
- - Added a search field to comboboxes, such as the model selector.
3857
- - Added a Duplicate option to the models editor.
3858
- - Added drag-and-drop to context list.
3859
- - Added multi-select, drag-and-drop, Cut, Copy, and Paste features to the File Explorer.
3860
- - Fix: scroll restoration after actions in the context list.
3861
- - Fix: 'Use as image' option in the File Explorer.
3862
- - Fix: current preset system prompt disappearing on profile change.
3863
- - Other UI fixes/improvements.
3864
-
3865
- **2.6.67 (2025-12-26)**
3866
-
3867
- - Added a provider filter to the models editor.
3868
- - Added video options (resolution, duration) to the toolbox.
3869
- - Updated the models configuration.
3870
-
3871
- **2.6.66 (2025-12-25)**
3872
-
3873
- - Added Sora 2 support - #155.
3874
- - Added Nano Banana support.
3875
- - Added Qdrant Vector Store - merged PR #147 by @Anush008.
3876
- - Added models: gpt-5.2, gpt-image-1.5, gemini-3, nano-banana-pro, sora-2, claude-sonnet-4.5, claude-opus-4.5, veo-3.1.
3877
- - Added Select/unselect All option in checkbox lists.
3878
- - OpenAI SDK upgraded to 2.14.0, Anthropic SDK upgraded to 0.75.0, xAI SDK upgraded to 1.5.0, Google GenAI upgraded to 1.56.0, LlamaIndex upgraded to 0.14.10.
3879
- - Fix: charset-normalizer 3.2.0 circular import - #152.
3880
- - Fix: Google client closed state.
3881
-
3882
3834
  # Credits and links
3883
3835
 
3884
3836
  **Official website:** <https://pygpt.net>