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,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: 2026.01.05 17:00:00 #
9
+ # Updated Date: 2026.01.21 01:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Dict
@@ -41,7 +41,7 @@ class Mapping:
41
41
  try:
42
42
  if getter() == v:
43
43
  continue
44
- except Exception:
44
+ except Exception as e:
45
45
  pass
46
46
  setter(v)
47
47
  except Exception:
@@ -91,6 +91,7 @@ class Mapping:
91
91
  # output
92
92
  nodes['output.timestamp'] = 'output.timestamp'
93
93
  nodes['output.edit'] = 'output.edit'
94
+ nodes['output.raw'] = 'output.raw'
94
95
 
95
96
  # painter
96
97
  nodes['painter.btn.brush'] = 'painter.mode.paint'
@@ -199,6 +200,12 @@ class Mapping:
199
200
  nodes['preset.tool.function.label.agent_llama'] = 'preset.tool.function.tip.agent_llama'
200
201
  nodes['preset.btn.current'] = 'dialog.preset.btn.current'
201
202
  nodes['preset.btn.save'] = 'dialog.preset.btn.save'
203
+ nodes['preset.editor.warn_label'] = 'preset.personalize.warning'
204
+ nodes['toolbox.model.label.label'] = 'toolbox.model.label'
205
+ nodes['preset.editor.personalize.avatar.choose'] = 'preset.personalize.avatar.choose'
206
+ nodes['preset.editor.personalize.avatar.remove'] = 'preset.personalize.avatar.remove'
207
+ nodes['preset.ai_avatar.label'] = 'preset.ai_avatar.label'
208
+ nodes['preset.ai_personalize.desc'] = 'preset.ai_personalize.desc'
202
209
 
203
210
  # dialog: rename
204
211
  nodes['dialog.rename.label'] = 'dialog.rename.title'
@@ -394,6 +401,11 @@ class Mapping:
394
401
  menu_text['debug.db'] = 'menu.debug.db'
395
402
  menu_text['debug.logger'] = 'menu.debug.logger'
396
403
  menu_text['debug.app.log'] = 'menu.debug.app.log'
404
+ menu_text['debug.render'] = 'menu.debug.render'
405
+ menu_text['debug.indexes'] = 'menu.debug.indexes'
406
+ menu_text['debug.kernel'] = 'menu.debug.kernel'
407
+ menu_text['debug.tabs'] = 'menu.debug.tabs'
408
+ menu_text['debug.fixtures.stream'] = 'menu.debug.fixtures.stream'
397
409
 
398
410
  dialog_title = {}
399
411
  dialog_title['info.about'] = 'dialog.about.title'
@@ -434,6 +446,7 @@ class Mapping:
434
446
  placeholders = {}
435
447
  placeholders['ctx.search'] = 'ctx.list.search.placeholder'
436
448
  placeholders['interpreter.input'] = 'interpreter.input.placeholder'
449
+ placeholders['input'] = 'input.placeholder'
437
450
 
438
451
  mapping = {}
439
452
  mapping['nodes'] = nodes
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.24 23:00:00 #
9
+ # Updated Date: 2026.01.22 16:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Optional, Tuple
@@ -64,22 +64,24 @@ class Notepad:
64
64
  self,
65
65
  idx: Optional[int] = None,
66
66
  tab: Optional[Tab] = None,
67
+ restore: bool = False
67
68
  ) -> Tuple[TabBody, int, int]:
68
69
  """
69
70
  Create notepad widget
70
71
 
71
72
  :param idx: notepad idx
72
73
  :param tab: existing tab to use (optional)
74
+ :param restore: whether to restore existing notepad (default: False)
73
75
  :return: notepad widget (TabBody)
74
76
  """
75
77
  tabs_core = self.window.core.tabs
76
- existing_tabs = tabs_core.get_tabs_by_type(Tab.TAB_NOTEPAD)
77
- used_ids = {t.data_id for t in existing_tabs}
78
-
79
- if idx is None:
80
- idx = 1
81
- while idx in used_ids:
82
- idx += 1
78
+ if not restore:
79
+ existing_tabs = tabs_core.get_tabs_by_type(Tab.TAB_NOTEPAD)
80
+ used_ids = {t.data_id for t in existing_tabs}
81
+ if idx is None:
82
+ idx = 1
83
+ while idx in used_ids:
84
+ idx += 1
83
85
 
84
86
  suffix = self.get_next_suffix()
85
87
 
@@ -116,22 +118,33 @@ class Notepad:
116
118
  self.update()
117
119
 
118
120
  def load(self):
119
- """Load all notepads contents"""
120
- self.window.core.notepad.load_all()
121
- items = self.window.core.notepad.get_all()
122
- num_notepads = self.get_num_notepads()
123
- if len(items) == 0:
124
- if num_notepads > 0:
125
- for idx in range(1, num_notepads + 1):
126
- item = NotepadItem()
127
- item.idx = idx
128
- items[idx] = item
121
+ """Load all notepads"""
122
+ core_notepad = self.window.core.notepad
123
+ prev_locked = core_notepad.locked
124
+ core_notepad.locked = True # block any autosave during restore
125
+ try:
126
+ core_notepad.load_all()
127
+ items = core_notepad.get_all()
128
+ num_notepads = self.get_num_notepads()
129
+
130
+ if len(items) == 0:
131
+ if num_notepads > 0:
132
+ for idx in range(1, num_notepads + 1):
133
+ item = NotepadItem()
134
+ item.idx = idx
135
+ items[idx] = item
129
136
 
130
- if num_notepads > 0:
131
- for idx, item in items.items():
132
- widget = self.window.ui.notepad.get(idx)
133
- if widget is not None:
134
- widget.setText(item.content)
137
+ if num_notepads > 0:
138
+ for idx, item in items.items():
139
+ widget = self.window.ui.notepad.get(idx)
140
+ if widget is not None:
141
+ widget.setText(item.content)
142
+ highlights = item.highlights
143
+ widget.textarea.set_highlights(highlights)
144
+ if item.scroll_pos is not None and item.scroll_pos != -1:
145
+ widget.textarea.set_scroll_pos(item.scroll_pos)
146
+ finally:
147
+ core_notepad.locked = prev_locked # restore previous lock state
135
148
 
136
149
  def get_notepad_name(self, idx: int):
137
150
  """
@@ -167,12 +180,36 @@ class Notepad:
167
180
 
168
181
  widget = self.window.ui.notepad.get(idx)
169
182
  if widget is not None:
183
+ if not widget.textarea.is_initialized():
184
+ return # do not save uninitialized notepads
170
185
  text = widget.toPlainText()
171
186
  if item.content != text:
172
187
  item.content = text
188
+ self.save_state(idx, persist=False)
173
189
  core_notepad.update(item)
190
+ else:
191
+ self.save_state(idx)
174
192
  self.update()
175
193
 
194
+ def save_state(self, idx: int, persist: bool = True):
195
+ """
196
+ Save highlights for given notepad idx into item.highlights
197
+ """
198
+ widget = self.window.ui.notepad.get(idx)
199
+ if widget is None:
200
+ return
201
+ highlights = widget.textarea.get_highlights()
202
+ scroll_pos = widget.textarea.get_scroll_pos()
203
+ core_notepad = self.window.core.notepad
204
+ item = core_notepad.get_by_id(idx)
205
+ if item is None or core_notepad.locked:
206
+ return
207
+ item.highlights = highlights
208
+ if widget.textarea.is_initialized():
209
+ item.scroll_pos = scroll_pos
210
+ if persist:
211
+ core_notepad.update(item)
212
+
176
213
  def save_all(self):
177
214
  """Save all notepads contents"""
178
215
  items = self.window.core.notepad.get_all()
@@ -189,8 +226,9 @@ class Notepad:
189
226
  text = widget.toPlainText()
190
227
  if prev_content != text:
191
228
  items[idx].content = text
229
+ self.save_state(idx, persist=False) # persist highlights for each notepad
192
230
  self.window.core.notepad.update(items[idx])
193
- self.update()
231
+ self.update()
194
232
 
195
233
  def setup(self):
196
234
  """Setup all notepads"""
@@ -320,6 +358,7 @@ class Notepad:
320
358
  widget = self.window.ui.notepad.get(idx)
321
359
  if widget is not None:
322
360
  widget.textarea.clear()
361
+ widget.textarea.clear_highlights()
323
362
  self.save(idx)
324
363
  return True
325
364
  return False
@@ -343,8 +382,12 @@ class Notepad:
343
382
  widget = self.window.ui.notepad.get(idx)
344
383
  if widget is None:
345
384
  return
385
+ # only autoscroll brand-new notepad once, if there is no saved scroll position
346
386
  if idx not in self.opened_idx:
347
- QTimer.singleShot(0, widget.scroll_to_bottom)
387
+ item = self.window.core.notepad.get_by_id(idx)
388
+ has_saved_pos = item is not None and item.scroll_pos not in (None, -1)
389
+ if not has_saved_pos:
390
+ QTimer.singleShot(0, widget.scroll_to_bottom)
348
391
  if not widget.opened:
349
392
  widget.opened = True
350
393
  if idx not in self.opened_idx:
@@ -6,12 +6,13 @@
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.28 09:35:00 #
9
+ # Updated Date: 2026.01.20 16:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import datetime
13
13
  import os
14
14
  import shutil
15
+ import uuid
15
16
  from typing import Any, Optional, Dict
16
17
 
17
18
  from PySide6.QtCore import Slot, Qt
@@ -922,6 +923,7 @@ class Editor:
922
923
  self.reload_all_custom_agent_options()
923
924
  if self.opened:
924
925
  self.init(self.current_id)
926
+
925
927
  def init(self, id: Optional[str] = None):
926
928
  """
927
929
  Initialize preset editor
@@ -935,8 +937,10 @@ class Editor:
935
937
  data = PresetItem()
936
938
  data.name = ""
937
939
  data.filename = ""
940
+ data.uuid = str(uuid.uuid4())
938
941
 
939
942
  if id is None:
943
+ self.current = None # RESET HERE is always required for new/avatar update
940
944
  self.experts.update_list()
941
945
  self.window.ui.config[self.id]['idx'].set_value("_") # reset idx combo if new preset
942
946
 
@@ -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: 2026.01.02 20:00:00 #
9
+ # Updated Date: 2026.01.20 16:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import re
@@ -24,14 +24,12 @@ from pygpt_net.core.types import (
24
24
  MODE_AGENT_OPENAI,
25
25
  )
26
26
  from pygpt_net.controller.presets.editor import Editor
27
- # Editor controller
28
27
  from pygpt_net.core.events import AppEvent
29
28
  from pygpt_net.item.preset import PresetItem
30
29
  from pygpt_net.utils import trans
31
30
 
32
31
 
33
32
  _FILENAME_SANITIZE_RE = re.compile(r'[^a-zA-Z0-9_\-\.]')
34
- # keep original validation (do not break other parts)
35
33
  _VALIDATE_FILENAME_RE = re.compile(r'[^\w\s\-\.]')
36
34
 
37
35
 
@@ -630,7 +628,12 @@ class Presets:
630
628
  w.core.assistants.load()
631
629
  w.core.remote_store.openai.load_all()
632
630
 
633
- def _nearest_id_after_delete(self, mode: str, idx: Optional[int], deleting_id: Optional[str]) -> Optional[str]:
631
+ def _nearest_id_after_delete(
632
+ self,
633
+ mode: str,
634
+ idx: Optional[int],
635
+ deleting_id: Optional[str]
636
+ ) -> Optional[str]:
634
637
  """
635
638
  Compute the nearest neighbor to select after deletion:
636
639
  - Prefer the next item (below) if exists;
@@ -847,11 +850,18 @@ class Presets:
847
850
  # Drag & drop ordering helpers
848
851
  # ----------------------------
849
852
 
850
- def persist_order_for_mode(self, mode: str, uuids: List[str]):
853
+ def persist_order_for_mode(
854
+ self,
855
+ mode: str,
856
+ uuids: List[str]
857
+ ):
851
858
  """
852
859
  Persist new order (by UUIDs) for given mode.
853
860
 
854
861
  The special '*' preset (current.<mode>) is not included here and always pinned at index 0.
862
+
863
+ :param mode: mode name
864
+ :param uuids: list of preset UUIDs in new order
855
865
  """
856
866
  w = self.window
857
867
  cfg = w.core.config
@@ -866,6 +876,8 @@ class Presets:
866
876
  def dnd_enabled(self) -> bool:
867
877
  """
868
878
  Check if drag and drop is globally enabled in config.
879
+
880
+ :return: True if enabled
869
881
  """
870
882
  try:
871
883
  return bool(self.window.core.config.get('presets.drag_and_drop.enabled'))
@@ -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.17 07:00:00 #
9
+ # Updated Date: 2026.01.07 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import Slot, QTimer
@@ -87,6 +87,8 @@ class Realtime:
87
87
  self.window.core.api.google.realtime.handle_audio_input(event)
88
88
  elif self.current_active == "openai":
89
89
  self.window.core.api.openai.realtime.handle_audio_input(event)
90
+ elif self.current_active == "x_ai":
91
+ self.window.core.api.xai.realtime.handle_audio_input(event)
90
92
 
91
93
  # begin: first text chunk or audio chunk received, start rendering
92
94
  elif event.name == RealtimeEvent.RT_OUTPUT_READY:
@@ -216,6 +218,8 @@ class Realtime:
216
218
  self.window.core.api.google.realtime.manual_commit()
217
219
  elif self.current_active == "openai":
218
220
  self.window.core.api.openai.realtime.manual_commit()
221
+ elif self.current_active == "x_ai":
222
+ self.window.core.api.xai.realtime.manual_commit()
219
223
 
220
224
  def end_turn(self, ctx):
221
225
  """
@@ -252,6 +256,10 @@ class Realtime:
252
256
  self.window.core.api.google.realtime.shutdown()
253
257
  except Exception as e:
254
258
  self.window.core.debug.log(f"[google] Realtime shutdown error: {e}")
259
+ try:
260
+ self.window.core.api.xai.realtime.shutdown()
261
+ except Exception as e:
262
+ self.window.core.debug.log(f"[xAI] Realtime shutdown error: {e}")
255
263
  try:
256
264
  self.manager.shutdown()
257
265
  except Exception as e:
@@ -267,6 +275,10 @@ class Realtime:
267
275
  self.window.core.api.google.realtime.reset()
268
276
  except Exception as e:
269
277
  self.window.core.debug.log(f"[google] Realtime reset error: {e}")
278
+ try:
279
+ self.window.core.api.xai.realtime.reset()
280
+ except Exception as e:
281
+ self.window.core.debug.log(f"[xAI] Realtime reset error: {e}")
270
282
 
271
283
  def is_supported(self) -> bool:
272
284
  """
@@ -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.26 13:00:00 #
9
+ # Updated Date: 2026.01.21 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -290,4 +290,13 @@ class Theme:
290
290
  if self.current_theme != self.window.core.config.get('theme'):
291
291
  self.setup()
292
292
  self.update_style()
293
- self.update_syntax()
293
+ self.update_syntax()
294
+
295
+ def is_dark_theme(self) -> bool:
296
+ """
297
+ Check if current theme is dark
298
+
299
+ :return: True if dark theme, False otherwise
300
+ """
301
+ current = self.window.core.config.get('theme')
302
+ return current.startswith('dark')
@@ -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.16 22:00:00 #
9
+ # Updated Date: 2026.01.22 04:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Any, Optional, Tuple
@@ -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.16 22:00:00 #
9
+ # Updated Date: 2026.01.22 04:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Optional, List, Dict
@@ -171,8 +171,11 @@ class Output:
171
171
  tabs = self.window.core.tabs
172
172
  active_pid = tabs.get_active_pid()
173
173
  tab = tabs.get_tab_by_pid(active_pid)
174
+ in_second_column = False
174
175
  if tab is None:
175
176
  return None
177
+
178
+ # 1) check current column
176
179
  col_idx = tab.column_idx
177
180
  col_map = self.mapping.get(col_idx, {})
178
181
  if not isinstance(col_map, dict):
@@ -182,7 +185,25 @@ class Output:
182
185
  for pid in candidates:
183
186
  if pid == active_pid: # prefer active PID
184
187
  return pid
185
- return candidates[-1] if candidates else None # return last found PID
188
+ pid = candidates[-1] if candidates else None
189
+
190
+ # 2) check if mapped in other columns
191
+ if pid is None:
192
+ for idx, other_col_map in self.mapping.items():
193
+ if idx == col_idx:
194
+ continue
195
+ for other_pid, meta_id in other_col_map.items():
196
+ if meta_id == meta.id:
197
+ in_second_column = True
198
+ pid = other_pid
199
+ break
200
+ if pid is not None:
201
+ break
202
+
203
+ if in_second_column and not self.window.controller.chat.input.generating:
204
+ return None # allow remapping only when not generating
205
+
206
+ return pid # return last found PID
186
207
 
187
208
  def is_empty(self) -> bool:
188
209
  """
@@ -214,15 +235,13 @@ class Output:
214
235
  active_pid = tabs.get_active_pid()
215
236
  mapped_pid = self.get_mapped(meta)
216
237
  if mapped_pid == active_pid:
217
- return active_pid
218
- return self.store(meta)
219
-
220
- def clear(self):
221
- """Clear mapping"""
222
- self.mapping.clear()
223
- self.last_pids.clear()
224
- self.last_pid = 0
225
- self.initialized = False
238
+ pid = active_pid
239
+ else:
240
+ if mapped_pid is None:
241
+ pid = self.store(meta)
242
+ else:
243
+ pid = mapped_pid
244
+ return pid
226
245
 
227
246
  def get_current(self, meta: Optional[CtxMeta] = None):
228
247
  """
@@ -313,4 +332,11 @@ class Output:
313
332
  if pid in self.last_pids:
314
333
  del self.last_pids[pid]
315
334
  if pid == self.last_pid:
316
- self.last_pid = 0
335
+ self.last_pid = 0
336
+
337
+ def clear(self):
338
+ """Clear mapping"""
339
+ self.mapping.clear()
340
+ self.last_pids.clear()
341
+ self.last_pid = 0
342
+ self.initialized = False
@@ -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: 2026.01.02 20:00:00 #
9
+ # Updated Date: 2026.01.22 16:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -162,6 +162,8 @@ class Database:
162
162
  'idx',
163
163
  'title',
164
164
  'content',
165
+ 'highlights_json',
166
+ 'scroll_pos',
165
167
  'created_ts',
166
168
  'updated_ts',
167
169
  'is_deleted',
@@ -287,7 +289,7 @@ class Database:
287
289
  'sort_by': columns["notepad"],
288
290
  'search_fields': ['id', 'title', 'content'],
289
291
  'timestamp_columns': ['created_ts', 'updated_ts'],
290
- 'json_columns': [],
292
+ 'json_columns': ['highlights_json'],
291
293
  'default_sort': 'id',
292
294
  'default_order': 'DESC',
293
295
  'primary_key': 'id',
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.24 23:00:00 #
9
+ # Updated Date: 2026.01.20 21:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtWidgets import QApplication
@@ -22,11 +22,39 @@ class Console:
22
22
  :param window: Window instance
23
23
  """
24
24
  self.window = window
25
+ # list of supported commands for auto-completion in ConsoleInput
26
+ self._supported_commands = [
27
+ "clr",
28
+ "mem",
29
+ "free",
30
+ "css",
31
+ "lang",
32
+ "oclr",
33
+ "dump(",
34
+ "js(",
35
+ "help",
36
+ "/help",
37
+ "/h",
38
+ "quit",
39
+ "exit",
40
+ "/q",
41
+ ]
42
+
43
+ def get_supported_commands(self):
44
+ """
45
+ Return list of supported commands for auto-completion.
46
+
47
+ :return: list of strings
48
+ """
49
+ return list(self._supported_commands)
25
50
 
26
51
  def on_send(self):
27
52
  """Called on send message from console"""
28
53
  msg = self.window.console.text().strip()
29
54
  if msg:
55
+ # push to history first to make it available for Up/Down
56
+ if hasattr(self.window.console, "add_to_history"):
57
+ self.window.console.add_to_history(msg)
30
58
  self.clear()
31
59
  self.log(msg)
32
60
  QApplication.processEvents()
@@ -121,4 +149,4 @@ class Console:
121
149
  " help - show this help message\n"
122
150
  " quit|exit - close application\n"
123
151
  " JS debug (window.*): STREAM_DEBUG|MD_LANG_DEBUG|CM_DEBUG\n"
124
- )
152
+ )
@@ -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.14 20:00:00 #
9
+ # Updated Date: 2026.01.22 04:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
@@ -47,6 +47,7 @@ class ContextDebug:
47
47
  debug.add(self.id, 'current (id)', str(ctx_core.get_current()))
48
48
  debug.add(self.id, 'len(meta)', len(ctx_core.meta))
49
49
  debug.add(self.id, 'len(items)', len(ctx_core.get_items()))
50
+ debug.add(self.id, 'Stream PIDs', str(controller.chat.stream.get_pid_ids()))
50
51
  debug.add(self.id, 'SYS PROMPT (current)', str(ctx_core.current_sys_prompt))
51
52
  debug.add(self.id, 'CMD (current)', str(ctx_core.current_cmd))
52
53
  debug.add(self.id, 'CMD schema (current)', str(ctx_core.current_cmd_schema))
@@ -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.14 20:00:00 #
9
+ # Updated Date: 2026.01.21 01:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
 
@@ -20,16 +20,38 @@ class UIDebug:
20
20
  self.window = window
21
21
  self.id = 'ui'
22
22
 
23
+ def _sortable_key(self, key):
24
+ """
25
+ Return a safe, comparable key representation for sorting across heterogeneous key types.
26
+ Uses str() with robust fallbacks to avoid TypeError.
27
+ """
28
+ try:
29
+ return str(key)
30
+ except Exception:
31
+ try:
32
+ return repr(key)
33
+ except Exception:
34
+ return f"{type(key).__name__}@{id(key)}"
35
+
36
+ def _sorted_items(self, d: dict):
37
+ """
38
+ Return dict items sorted by key at this nesting level.
39
+ """
40
+ return sorted(d.items(), key=lambda kv: self._sortable_key(kv[0]))
41
+
23
42
  def map_structure(self, d, show_value: bool = False):
24
43
  """
25
44
  Map dict structure
26
45
 
27
46
  :param d: data to map
28
47
  :param show_value: show value
29
- :return: mapped structure
48
+ :return: mapped structure with keys sorted at every nesting level
30
49
  """
31
50
  if isinstance(d, dict):
32
- return {k: self.map_structure(v, show_value) for k, v in d.items()}
51
+ out = {}
52
+ for k, v in self._sorted_items(d):
53
+ out[k] = self.map_structure(v, show_value)
54
+ return out
33
55
  elif isinstance(d, list):
34
56
  return [self.map_structure(item, show_value) for item in d]
35
57
  else:
@@ -74,4 +96,4 @@ class UIDebug:
74
96
  self.update_section(ui.splitters, 'splitters')
75
97
  self.update_section(ui.tabs, 'tabs')
76
98
 
77
- debug.end(self.id)
99
+ debug.end(self.id)
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.07.16 02:00:00 #
9
+ # Updated Date: 2026.01.23 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -149,11 +149,12 @@ class Filesystem:
149
149
  return str(os.path.join(*parts)) # rebuild OS directory separators
150
150
  return path
151
151
 
152
- def to_workdir(self, path: str) -> str:
152
+ def to_workdir(self, path: str, auto_prefix: bool = True) -> str:
153
153
  """
154
154
  Replace user path with current workdir
155
155
 
156
156
  :param path: path to fix
157
+ :param auto_prefix: add workdir prefix if missing
157
158
  :return: path with replaced user workdir
158
159
  """
159
160
  path = self.get_path(path)
@@ -166,6 +167,9 @@ class Filesystem:
166
167
  work_dir
167
168
  )
168
169
 
170
+ if not auto_prefix:
171
+ return path
172
+
169
173
  # try to find workdir in path: old versions compatibility, < 2.0.113
170
174
  if work_dir.endswith('.config/pygpt-net'):
171
175
  work_dir = work_dir.rsplit('/.config/pygpt-net', 1)[0]