pygpt-net 2.6.46__py3-none-any.whl → 2.6.48__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 (40) hide show
  1. pygpt_net/CHANGELOG.txt +9 -0
  2. pygpt_net/__init__.py +1 -1
  3. pygpt_net/app.py +5 -0
  4. pygpt_net/app_core.py +39 -39
  5. pygpt_net/controller/__init__.py +72 -64
  6. pygpt_net/controller/audio/audio.py +2 -0
  7. pygpt_net/controller/chat/text.py +2 -1
  8. pygpt_net/controller/ctx/common.py +0 -7
  9. pygpt_net/controller/ctx/ctx.py +172 -6
  10. pygpt_net/controller/ctx/extra.py +3 -3
  11. pygpt_net/controller/notepad/notepad.py +0 -2
  12. pygpt_net/controller/settings/editor.py +3 -1
  13. pygpt_net/controller/theme/common.py +8 -2
  14. pygpt_net/controller/theme/theme.py +5 -5
  15. pygpt_net/controller/ui/tabs.py +9 -2
  16. pygpt_net/core/ctx/ctx.py +79 -26
  17. pygpt_net/core/render/web/renderer.py +2 -0
  18. pygpt_net/core/tabs/tab.py +2 -2
  19. pygpt_net/core/tabs/tabs.py +57 -10
  20. pygpt_net/data/config/config.json +2 -2
  21. pygpt_net/data/config/models.json +2 -2
  22. pygpt_net/data/css/web-blocks.css +256 -270
  23. pygpt_net/data/css/web-chatgpt.css +276 -301
  24. pygpt_net/data/css/web-chatgpt_wide.css +286 -294
  25. pygpt_net/data/js/app.js +1218 -1186
  26. pygpt_net/js_rc.py +14192 -14641
  27. pygpt_net/provider/core/config/patch.py +9 -0
  28. pygpt_net/provider/core/ctx/db_sqlite/storage.py +19 -5
  29. pygpt_net/ui/__init__.py +9 -14
  30. pygpt_net/ui/layout/chat/chat.py +2 -2
  31. pygpt_net/ui/layout/ctx/ctx_list.py +71 -1
  32. pygpt_net/ui/widget/lists/base.py +32 -1
  33. pygpt_net/ui/widget/lists/context.py +45 -2
  34. pygpt_net/ui/widget/tabs/body.py +11 -3
  35. pygpt_net/ui/widget/textarea/notepad.py +0 -4
  36. {pygpt_net-2.6.46.dist-info → pygpt_net-2.6.48.dist-info}/METADATA +11 -2
  37. {pygpt_net-2.6.46.dist-info → pygpt_net-2.6.48.dist-info}/RECORD +40 -40
  38. {pygpt_net-2.6.46.dist-info → pygpt_net-2.6.48.dist-info}/LICENSE +0 -0
  39. {pygpt_net-2.6.46.dist-info → pygpt_net-2.6.48.dist-info}/WHEEL +0 -0
  40. {pygpt_net-2.6.46.dist-info → pygpt_net-2.6.48.dist-info}/entry_points.txt +0 -0
@@ -88,6 +88,15 @@ class Patch:
88
88
  patch_css('web-chatgpt_wide.light.css', True)
89
89
  updated = True
90
90
 
91
+ # < 2.6.48
92
+ if old < parse_version("2.6.48"):
93
+ print("Migrating config from < 2.6.48...")
94
+ # reformat
95
+ patch_css('web-chatgpt.css', True)
96
+ patch_css('web-chatgpt_wide.css', True)
97
+ patch_css('web-blocks.css', True)
98
+ updated = True
99
+
91
100
  # update file
92
101
  migrated = False
93
102
  if updated:
@@ -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: 2024.12.14 22:00:00 #
9
+ # Updated Date: 2025.09.15 22:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from datetime import datetime
@@ -109,6 +109,12 @@ class Storage:
109
109
  continue
110
110
  mode = filter.get('mode', '=')
111
111
  value = filter.get('value', '')
112
+
113
+ # handle special case for "ungrouped" (group_id IS NULL OR = 0)
114
+ if key == 'group_id' and str(mode).upper() == 'NULL_OR_ZERO':
115
+ where_clauses.append("(m.group_id IS NULL OR m.group_id = 0)")
116
+ continue
117
+
112
118
  key_name = 'm.' + key
113
119
  if isinstance(value, int):
114
120
  where_clauses.append(f"{key_name} {mode} :{key}")
@@ -116,7 +122,7 @@ class Storage:
116
122
  elif isinstance(value, str):
117
123
  where_clauses.append(f"{key_name} {mode} :{key}")
118
124
  bind_params[key] = f"%{value}%"
119
- elif isinstance(value, list):
125
+ elif isinstance(value, list) and len(value) > 0:
120
126
  values = "(" + ",".join([str(x) for x in value]) + ")"
121
127
  where_clauses.append(f"{key_name} {mode} {values}")
122
128
 
@@ -148,15 +154,21 @@ class Storage:
148
154
  :return: dict of CtxMeta
149
155
  """
150
156
  limit_suffix = ""
151
- if limit is not None and limit > 0:
152
- limit_suffix = " LIMIT {}".format(limit)
153
-
154
157
  where_statement, join_statement, bind_params = self.prepare_query(
155
158
  search_string=search_string,
156
159
  filters=filters,
157
160
  search_content=search_content,
158
161
  append_date_ranges=True,
159
162
  )
163
+
164
+ # Build LIMIT/OFFSET only when limit > 0; LIMIT 0 would mean "no rows"
165
+ if limit is not None and int(limit) > 0:
166
+ limit_suffix = " LIMIT :limit"
167
+ bind_params['limit'] = int(limit)
168
+ if offset is not None and int(offset) > 0:
169
+ limit_suffix += " OFFSET :offset"
170
+ bind_params['offset'] = int(offset)
171
+
160
172
  stmt_text = f"""
161
173
  SELECT
162
174
  m.*,
@@ -168,6 +180,8 @@ class Storage:
168
180
  {join_statement}
169
181
  WHERE
170
182
  {where_statement}
183
+ GROUP BY
184
+ m.id
171
185
  ORDER BY
172
186
  m.updated_ts DESC {limit_suffix}
173
187
  """
pygpt_net/ui/__init__.py CHANGED
@@ -16,13 +16,13 @@ from PySide6.QtCore import Qt, QTimer
16
16
  from PySide6.QtGui import QFontDatabase, QIcon
17
17
  from PySide6.QtWidgets import QSplitter, QMessageBox
18
18
 
19
- from pygpt_net.ui.base.context_menu import ContextMenu
20
- from pygpt_net.ui.dialogs import Dialogs
21
- from pygpt_net.ui.layout.chat import ChatMain
22
- from pygpt_net.ui.layout.ctx import CtxMain
23
- from pygpt_net.ui.layout.toolbox import ToolboxMain
24
- from pygpt_net.ui.menu import Menu
25
- from pygpt_net.ui.tray import Tray
19
+ from .base.context_menu import ContextMenu
20
+ from .dialogs import Dialogs
21
+ from .layout.chat import ChatMain
22
+ from .layout.ctx import CtxMain
23
+ from .layout.toolbox import ToolboxMain
24
+ from .menu import Menu
25
+ from .tray import Tray
26
26
 
27
27
 
28
28
  class UI:
@@ -139,12 +139,7 @@ class UI:
139
139
  suffix = self.window.core.platforms.get_env_suffix()
140
140
  profile_name = self.window.core.config.profile.get_current_name()
141
141
  self.window.setWindowTitle(
142
- 'PyGPT - Desktop AI Assistant {} | build {}{} ({})'.format(
143
- self.window.meta['version'],
144
- self.window.meta['build'].replace('.', '-'),
145
- suffix,
146
- profile_name,
147
- )
142
+ f"PyGPT - Desktop AI Assistant {self.window.meta['version']} | build {self.window.meta['build'].replace('.', '-')}{suffix} ({profile_name})"
148
143
  )
149
144
 
150
145
  def post_setup(self):
@@ -164,7 +159,7 @@ class UI:
164
159
  return
165
160
  msg = str(text)
166
161
  msg = msg.replace("\n", " ")
167
- status = msg[:self.STATUS_MAX_CHARS] + '...' if len(msg) > self.STATUS_MAX_CHARS else msg # truncate
162
+ status = f"{msg[:self.STATUS_MAX_CHARS]}..." if len(msg) > self.STATUS_MAX_CHARS else msg # truncate
168
163
  self.nodes['status'].setText(status)
169
164
 
170
165
  def get_status(self):
@@ -12,8 +12,8 @@
12
12
  from PySide6.QtCore import Qt, Slot
13
13
  from PySide6.QtWidgets import QSplitter
14
14
 
15
- from pygpt_net.ui.layout.chat.input import Input
16
- from pygpt_net.ui.layout.chat.output import Output
15
+ from .input import Input
16
+ from .output import Output
17
17
 
18
18
 
19
19
  class ChatMain:
@@ -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.12 23:00:00 #
9
+ # Updated Date: 2025.09.15 22:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6 import QtCore
@@ -113,9 +113,79 @@ class CtxList:
113
113
  self.update_items_pinned(id, data)
114
114
  self.update_items(id, data)
115
115
  self.update_groups(id, data, expand=expand)
116
+
117
+ # APPLY PENDING SCROLL BEFORE RE-ENABLING UPDATES (prevents top flicker)
118
+ try:
119
+ node.apply_pending_scroll()
120
+ node.clear_pending_scroll()
121
+ except Exception:
122
+ pass
116
123
  finally:
117
124
  node.setUpdatesEnabled(True)
118
125
 
126
+ def _find_first_group_row(self, model) -> int:
127
+ """Find the row index of the first GroupItem; return -1 if none."""
128
+ for r in range(model.rowCount()):
129
+ it = model.item(r)
130
+ if isinstance(it, GroupItem):
131
+ return r
132
+ return -1
133
+
134
+ def append_unpaginated(self, id: str, data: dict, add_ids: list[int]):
135
+ """
136
+ Append more ungrouped and not pinned items without rebuilding the model.
137
+ Keeps scroll position perfectly stable.
138
+ """
139
+ if not add_ids:
140
+ return
141
+ node = self.window.ui.nodes[id]
142
+ model = self.window.ui.models[id]
143
+
144
+ folders_top = bool(self.window.core.config.get("ctx.records.folders.top"))
145
+ # decide insertion point: at the end, or just before the first group row
146
+ insert_pos = model.rowCount()
147
+ if not folders_top:
148
+ grp_idx = self._find_first_group_row(model)
149
+ insert_pos = grp_idx if grp_idx >= 0 else model.rowCount()
150
+
151
+ # find last dt of existing ungrouped area before insertion point (for date sections)
152
+ last_dt_str = None
153
+ for r in range(insert_pos - 1, -1, -1):
154
+ it = model.item(r)
155
+ if isinstance(it, Item):
156
+ data_role = it.data(QtCore.Qt.ItemDataRole.UserRole) or {}
157
+ if not data_role.get("in_group", False) and not data_role.get("is_important", False):
158
+ last_dt_str = getattr(it, "dt", None)
159
+ break
160
+ elif isinstance(it, GroupItem):
161
+ break # hit groups boundary going upwards
162
+ else:
163
+ # SectionItem or others – skip
164
+ continue
165
+
166
+ node.setUpdatesEnabled(False)
167
+ try:
168
+ # append strictly in the order provided by add_ids (older first)
169
+ for mid in add_ids:
170
+ meta = data.get(mid)
171
+ if meta is None:
172
+ continue
173
+ item = self.build_item(mid, meta, is_group=False)
174
+
175
+ # Optional date sections (same logic as in update_items)
176
+ if self._group_separators and (not item.isPinned or self._pinned_separators):
177
+ if last_dt_str is None or last_dt_str != item.dt:
178
+ section = self.build_date_section(item.dt, group=False)
179
+ if section:
180
+ model.insertRow(insert_pos, section)
181
+ insert_pos += 1
182
+ last_dt_str = item.dt
183
+
184
+ model.insertRow(insert_pos, item)
185
+ insert_pos += 1
186
+ finally:
187
+ node.setUpdatesEnabled(True)
188
+
119
189
  def update_items(self, id, data):
120
190
  """
121
191
  Update items
@@ -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.19 17:00:00 #
9
+ # Updated Date: 2025.09.15 22:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import QItemSelectionModel
@@ -36,6 +36,10 @@ class BaseList(QTreeView):
36
36
  self.v_scroll_value = 0
37
37
  self.h_scroll_value = 0
38
38
 
39
+ # pending scroll values applied while updates are disabled (to avoid top flicker)
40
+ self._pending_v_scroll_value = None
41
+ self._pending_h_scroll_value = None
42
+
39
43
  def click(self, val):
40
44
  self.window.controller.mode.select(self.id)
41
45
  self.selection = self.selectionModel().selection()
@@ -103,3 +107,30 @@ class BaseList(QTreeView):
103
107
  """Restore scroll position"""
104
108
  self.verticalScrollBar().setValue(self.v_scroll_value)
105
109
  self.horizontalScrollBar().setValue(self.h_scroll_value)
110
+
111
+ def set_pending_v_scroll(self, value: int):
112
+ """
113
+ Set vertical scroll value to apply while updates are disabled.
114
+ This prevents a visible jump to the top during model rebuild.
115
+ """
116
+ self._pending_v_scroll_value = int(value)
117
+
118
+ def set_pending_h_scroll(self, value: int):
119
+ """Optional: set horizontal pending value."""
120
+ self._pending_h_scroll_value = int(value)
121
+
122
+ def clear_pending_scroll(self):
123
+ """Clear pending scroll values."""
124
+ self._pending_v_scroll_value = None
125
+ self._pending_h_scroll_value = None
126
+
127
+ def apply_pending_scroll(self):
128
+ """
129
+ Apply pending scroll values immediately.
130
+ IMPORTANT: Call this before re-enabling updates to avoid repaint at top.
131
+ """
132
+ if self._pending_v_scroll_value is not None:
133
+ self.verticalScrollBar().setValue(self._pending_v_scroll_value)
134
+ if self._pending_h_scroll_value is not None:
135
+ self.horizontalScrollBar().setValue(self._pending_h_scroll_value)
136
+ # do not clear here; let caller decide when to clear
@@ -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.12 23:47:47 #
9
+ # Updated Date: 2025.09.15 22:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import datetime
@@ -17,7 +17,7 @@ from PySide6.QtCore import Qt, QPoint, QItemSelectionModel
17
17
  from PySide6.QtGui import QIcon, QColor, QPixmap, QStandardItem
18
18
  from PySide6.QtWidgets import QMenu
19
19
 
20
- from pygpt_net.ui.widget.lists.base import BaseList
20
+ from .base import BaseList
21
21
  from pygpt_net.utils import trans
22
22
 
23
23
 
@@ -67,6 +67,30 @@ class ContextList(BaseList):
67
67
  # Safe no-op if the underlying view does not support setIndentation
68
68
  pass
69
69
 
70
+ self._loading_more = False # guard to avoid multiple triggers while updating
71
+ try:
72
+ self.verticalScrollBar().valueChanged.connect(self._on_vertical_scroll)
73
+ except Exception:
74
+ pass # safe no-op if view doesn't expose verticalScrollBar
75
+
76
+ def _on_vertical_scroll(self, value: int):
77
+ """
78
+ Trigger infinite scroll: when scrollbar reaches bottom, request the next page.
79
+ """
80
+ try:
81
+ sb = self.verticalScrollBar()
82
+ except Exception:
83
+ return
84
+ if sb.maximum() <= 0:
85
+ return # nothing to scroll
86
+ # Close-to-bottom detection; keep a tiny threshold for stability
87
+ if not self._loading_more and value >= sb.maximum():
88
+ self._loading_more = True
89
+ # Ask controller to increase the total limit and refresh the list
90
+ self.window.controller.ctx.load_more()
91
+ # Release the guard shortly after model updates
92
+ QtCore.QTimer.singleShot(250, lambda: setattr(self, "_loading_more", False))
93
+
70
94
  @property
71
95
  def _model(self):
72
96
  return self.window.ui.models['ctx.list']
@@ -292,6 +316,25 @@ class ContextList(BaseList):
292
316
  self.restore_after_ctx_menu = True
293
317
  self.restore_scroll_position()
294
318
 
319
+ def get_visible_unpaged_ids(self) -> set:
320
+ """
321
+ Return a set of IDs for currently visible, ungrouped and not pinned items (top-level only).
322
+ """
323
+ ids = set()
324
+ model = self._model
325
+ for r in range(model.rowCount()):
326
+ it = model.item(r)
327
+ # skip groups and date sections
328
+ if isinstance(it, GroupItem) or isinstance(it, SectionItem):
329
+ continue
330
+ if isinstance(it, Item):
331
+ data = it.data(QtCore.Qt.ItemDataRole.UserRole) or {}
332
+ in_group = bool(data.get("in_group", False))
333
+ is_important = bool(data.get("is_important", False))
334
+ if not in_group and not is_important and hasattr(it, "id"):
335
+ ids.add(int(it.id))
336
+ return ids
337
+
295
338
  def action_open(self, id: int, idx: int = None):
296
339
  """
297
340
  Open context action handler
@@ -31,7 +31,7 @@ class TabBody(QTabWidget):
31
31
  """
32
32
  Clean up on delete
33
33
  """
34
- if self.on_delete:
34
+ if self.on_delete and callable(self.on_delete):
35
35
  self.on_delete(self)
36
36
  self.delete_refs()
37
37
 
@@ -49,10 +49,18 @@ class TabBody(QTabWidget):
49
49
  Delete all references to widgets in this tab
50
50
  """
51
51
  for ref in self.refs:
52
+ if ref is None:
53
+ continue
52
54
  if ref and hasattr(ref, 'on_delete'):
53
- ref.on_delete()
55
+ try:
56
+ ref.on_delete()
57
+ except Exception:
58
+ pass
54
59
  if ref and hasattr(ref, 'deleteLater'):
55
- ref.deleteLater()
60
+ try:
61
+ ref.deleteLater()
62
+ except Exception:
63
+ pass
56
64
  del self.refs[:]
57
65
 
58
66
  def delete_ref(self, widget: Any) -> None:
@@ -120,10 +120,6 @@ class NotepadOutput(QTextEdit):
120
120
  if self.finder:
121
121
  self.finder.disconnect() # disconnect finder
122
122
  self.finder = None # delete finder
123
- try:
124
- self._vscroll.valueChanged.disconnect(self._on_scrollbar_value_changed)
125
- except Exception:
126
- pass
127
123
  self.deleteLater()
128
124
 
129
125
  def showEvent(self, event):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pygpt-net
3
- Version: 2.6.46
3
+ Version: 2.6.48
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, 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
@@ -118,7 +118,7 @@ Description-Content-Type: text/markdown
118
118
 
119
119
  [![pygpt](https://snapcraft.io/pygpt/badge.svg)](https://snapcraft.io/pygpt)
120
120
 
121
- Release: **2.6.46** | build: **2025-09-15** | Python: **>=3.10, <3.14**
121
+ Release: **2.6.48** | build: **2025-09-15** | Python: **>=3.10, <3.14**
122
122
 
123
123
  > Official website: https://pygpt.net | Documentation: https://pygpt.readthedocs.io
124
124
  >
@@ -3612,6 +3612,15 @@ may consume additional tokens that are not displayed in the main window.
3612
3612
 
3613
3613
  ## Recent changes:
3614
3614
 
3615
+ **2.6.48 (2025-09-15)**
3616
+
3617
+ - Added: auto-loading of next items to the list of contexts when scrolling to the end of the list.
3618
+
3619
+ **2.6.47 (2025-09-15)**
3620
+
3621
+ - Improved: Parsing of custom markup tags.
3622
+ - Optimized: Switching profiles.
3623
+
3615
3624
  **2.6.46 (2025-09-15)**
3616
3625
 
3617
3626
  - Added: Global proxy settings for all API SDKs.