pygpt-net 2.6.1__py3-none-any.whl → 2.6.6__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 (131) hide show
  1. pygpt_net/CHANGELOG.txt +23 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +20 -1
  4. pygpt_net/config.py +55 -65
  5. pygpt_net/controller/__init__.py +5 -2
  6. pygpt_net/controller/calendar/note.py +101 -126
  7. pygpt_net/controller/chat/chat.py +38 -35
  8. pygpt_net/controller/chat/render.py +154 -214
  9. pygpt_net/controller/chat/response.py +5 -3
  10. pygpt_net/controller/chat/stream.py +92 -27
  11. pygpt_net/controller/config/config.py +39 -42
  12. pygpt_net/controller/config/field/checkbox.py +16 -12
  13. pygpt_net/controller/config/field/checkbox_list.py +36 -31
  14. pygpt_net/controller/config/field/cmd.py +51 -57
  15. pygpt_net/controller/config/field/combo.py +33 -16
  16. pygpt_net/controller/config/field/dictionary.py +48 -55
  17. pygpt_net/controller/config/field/input.py +50 -32
  18. pygpt_net/controller/config/field/slider.py +40 -45
  19. pygpt_net/controller/config/field/textarea.py +20 -6
  20. pygpt_net/controller/config/placeholder.py +110 -231
  21. pygpt_net/controller/ctx/common.py +48 -48
  22. pygpt_net/controller/ctx/ctx.py +91 -132
  23. pygpt_net/controller/lang/mapping.py +57 -95
  24. pygpt_net/controller/lang/plugins.py +64 -55
  25. pygpt_net/controller/lang/settings.py +39 -38
  26. pygpt_net/controller/layout/layout.py +176 -109
  27. pygpt_net/controller/mode/mode.py +88 -85
  28. pygpt_net/controller/model/model.py +73 -73
  29. pygpt_net/controller/plugins/plugins.py +209 -223
  30. pygpt_net/controller/plugins/presets.py +54 -55
  31. pygpt_net/controller/plugins/settings.py +54 -69
  32. pygpt_net/controller/presets/editor.py +33 -88
  33. pygpt_net/controller/presets/experts.py +20 -1
  34. pygpt_net/controller/presets/presets.py +293 -298
  35. pygpt_net/controller/settings/profile.py +16 -4
  36. pygpt_net/controller/theme/theme.py +72 -81
  37. pygpt_net/controller/ui/mode.py +118 -186
  38. pygpt_net/controller/ui/tabs.py +69 -90
  39. pygpt_net/controller/ui/ui.py +47 -56
  40. pygpt_net/controller/ui/vision.py +24 -23
  41. pygpt_net/core/agents/runner.py +15 -7
  42. pygpt_net/core/bridge/bridge.py +5 -5
  43. pygpt_net/core/command/command.py +149 -219
  44. pygpt_net/core/ctx/ctx.py +94 -146
  45. pygpt_net/core/debug/debug.py +48 -58
  46. pygpt_net/core/experts/experts.py +3 -3
  47. pygpt_net/core/models/models.py +74 -112
  48. pygpt_net/core/modes/modes.py +13 -21
  49. pygpt_net/core/plugins/plugins.py +154 -177
  50. pygpt_net/core/presets/presets.py +103 -176
  51. pygpt_net/core/render/web/body.py +217 -215
  52. pygpt_net/core/render/web/renderer.py +330 -474
  53. pygpt_net/core/text/utils.py +28 -44
  54. pygpt_net/core/tokens/tokens.py +104 -203
  55. pygpt_net/data/config/config.json +3 -3
  56. pygpt_net/data/config/models.json +3 -3
  57. pygpt_net/data/locale/locale.de.ini +2 -0
  58. pygpt_net/data/locale/locale.en.ini +2 -0
  59. pygpt_net/data/locale/locale.es.ini +2 -0
  60. pygpt_net/data/locale/locale.fr.ini +2 -0
  61. pygpt_net/data/locale/locale.it.ini +2 -0
  62. pygpt_net/data/locale/locale.pl.ini +3 -1
  63. pygpt_net/data/locale/locale.uk.ini +2 -0
  64. pygpt_net/data/locale/locale.zh.ini +2 -0
  65. pygpt_net/item/ctx.py +141 -139
  66. pygpt_net/plugin/agent/plugin.py +2 -1
  67. pygpt_net/plugin/audio_output/plugin.py +5 -2
  68. pygpt_net/plugin/base/plugin.py +101 -85
  69. pygpt_net/plugin/bitbucket/__init__.py +12 -0
  70. pygpt_net/plugin/bitbucket/config.py +267 -0
  71. pygpt_net/plugin/bitbucket/plugin.py +126 -0
  72. pygpt_net/plugin/bitbucket/worker.py +569 -0
  73. pygpt_net/plugin/cmd_code_interpreter/plugin.py +3 -2
  74. pygpt_net/plugin/cmd_custom/plugin.py +3 -2
  75. pygpt_net/plugin/cmd_files/plugin.py +3 -2
  76. pygpt_net/plugin/cmd_history/plugin.py +3 -2
  77. pygpt_net/plugin/cmd_mouse_control/plugin.py +5 -2
  78. pygpt_net/plugin/cmd_serial/plugin.py +3 -2
  79. pygpt_net/plugin/cmd_system/plugin.py +3 -6
  80. pygpt_net/plugin/cmd_web/plugin.py +3 -2
  81. pygpt_net/plugin/experts/plugin.py +2 -2
  82. pygpt_net/plugin/facebook/__init__.py +12 -0
  83. pygpt_net/plugin/facebook/config.py +359 -0
  84. pygpt_net/plugin/facebook/plugin.py +113 -0
  85. pygpt_net/plugin/facebook/worker.py +698 -0
  86. pygpt_net/plugin/github/__init__.py +12 -0
  87. pygpt_net/plugin/github/config.py +441 -0
  88. pygpt_net/plugin/github/plugin.py +126 -0
  89. pygpt_net/plugin/github/worker.py +674 -0
  90. pygpt_net/plugin/google/__init__.py +12 -0
  91. pygpt_net/plugin/google/config.py +367 -0
  92. pygpt_net/plugin/google/plugin.py +126 -0
  93. pygpt_net/plugin/google/worker.py +826 -0
  94. pygpt_net/plugin/idx_llama_index/plugin.py +3 -2
  95. pygpt_net/plugin/mailer/plugin.py +3 -5
  96. pygpt_net/plugin/openai_vision/plugin.py +3 -2
  97. pygpt_net/plugin/real_time/plugin.py +52 -60
  98. pygpt_net/plugin/slack/__init__.py +12 -0
  99. pygpt_net/plugin/slack/config.py +349 -0
  100. pygpt_net/plugin/slack/plugin.py +115 -0
  101. pygpt_net/plugin/slack/worker.py +639 -0
  102. pygpt_net/plugin/telegram/__init__.py +12 -0
  103. pygpt_net/plugin/telegram/config.py +308 -0
  104. pygpt_net/plugin/telegram/plugin.py +117 -0
  105. pygpt_net/plugin/telegram/worker.py +563 -0
  106. pygpt_net/plugin/twitter/__init__.py +12 -0
  107. pygpt_net/plugin/twitter/config.py +491 -0
  108. pygpt_net/plugin/twitter/plugin.py +125 -0
  109. pygpt_net/plugin/twitter/worker.py +837 -0
  110. pygpt_net/provider/agents/llama_index/legacy/openai_assistant.py +35 -3
  111. pygpt_net/tools/code_interpreter/tool.py +0 -1
  112. pygpt_net/tools/translator/tool.py +1 -1
  113. pygpt_net/ui/base/config_dialog.py +86 -100
  114. pygpt_net/ui/base/context_menu.py +48 -46
  115. pygpt_net/ui/dialog/preset.py +34 -77
  116. pygpt_net/ui/layout/ctx/ctx_list.py +10 -6
  117. pygpt_net/ui/layout/toolbox/presets.py +41 -41
  118. pygpt_net/ui/main.py +49 -31
  119. pygpt_net/ui/tray.py +61 -60
  120. pygpt_net/ui/widget/calendar/select.py +86 -70
  121. pygpt_net/ui/widget/lists/attachment.py +86 -44
  122. pygpt_net/ui/widget/lists/base_list_combo.py +85 -33
  123. pygpt_net/ui/widget/lists/context.py +135 -188
  124. pygpt_net/ui/widget/lists/preset.py +59 -61
  125. pygpt_net/ui/widget/textarea/web.py +161 -48
  126. pygpt_net/utils.py +8 -1
  127. {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.6.dist-info}/METADATA +164 -2
  128. {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.6.dist-info}/RECORD +131 -103
  129. {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.6.dist-info}/LICENSE +0 -0
  130. {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.6.dist-info}/WHEEL +0 -0
  131. {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.6.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,4 @@
1
- # !/usr/bin/env python3
1
+ #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
3
  # ================================================== #
4
4
  # This file is a part of PYGPT package #
@@ -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.08 23:00:00 #
9
+ # Updated Date: 2025.08.15 03:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Any, Optional, Tuple
@@ -43,17 +43,19 @@ class Tabs:
43
43
 
44
44
  def setup(self):
45
45
  """Setup tabs"""
46
- self.window.core.tabs.load()
47
- self.window.controller.notepad.load()
46
+ w = self.window
47
+ w.core.tabs.load()
48
+ w.controller.notepad.load()
48
49
  self.setup_options()
49
50
  self.initialized = True
50
51
 
51
52
  def setup_options(self):
52
53
  """Setup options"""
53
- state = self.window.core.config.get("layout.split", False)
54
- self.window.ui.nodes['layout.split'].setChecked(state)
54
+ w = self.window
55
+ state = w.core.config.get("layout.split", False)
56
+ w.ui.nodes['layout.split'].setChecked(state)
55
57
  if not state:
56
- self.window.ui.splitters['columns'].setSizes([1, 0])
58
+ w.ui.splitters['columns'].setSizes([1, 0])
57
59
 
58
60
  def debug(self):
59
61
  """Debug tabs if enabled"""
@@ -103,15 +105,15 @@ class Tabs:
103
105
  :param idx: Tab index
104
106
  :param column_idx: Column index
105
107
  """
106
- self.appended = True # lock reload in previous tab
107
- self.column_idx = column_idx # switch to column
108
+ self.appended = True
109
+ self.column_idx = column_idx
108
110
  tab = self.window.core.tabs.append(
109
111
  type=type,
110
112
  idx=idx,
111
113
  column_idx=column_idx,
112
114
  tool_id=tool_id
113
115
  )
114
- self.switch_tab_by_idx(tab.idx, column_idx) # switch to new tab
116
+ self.switch_tab_by_idx(tab.idx, column_idx)
115
117
  self.debug()
116
118
 
117
119
  def reload_titles(self):
@@ -138,16 +140,21 @@ class Tabs:
138
140
 
139
141
  def reload_after(self):
140
142
  """Reload tabs after"""
141
- for pid in self.window.ui.nodes['output']:
142
- try:
143
- if self.window.core.config.get("render.plain") is True:
144
- self.window.ui.nodes['output_plain'][pid].setVisible(True)
145
- self.window.ui.nodes['output'][pid].setVisible(False)
146
- else:
147
- self.window.ui.nodes['output_plain'][pid].setVisible(False)
148
- self.window.ui.nodes['output'][pid].setVisible(True)
149
- except Exception as e:
150
- pass
143
+ w = self.window
144
+ plain = w.core.config.get("render.plain") is True
145
+ outputs = w.ui.nodes['output']
146
+ outputs_plain = w.ui.nodes['output_plain']
147
+ for pid in outputs:
148
+ out_plain = outputs_plain.get(pid)
149
+ out = outputs.get(pid)
150
+ if out_plain is None or out is None:
151
+ continue
152
+ if plain:
153
+ out_plain.setVisible(True)
154
+ out.setVisible(False)
155
+ else:
156
+ out_plain.setVisible(False)
157
+ out.setVisible(True)
151
158
  self.debug()
152
159
 
153
160
  def on_tab_changed(
@@ -161,7 +168,11 @@ class Tabs:
161
168
  :param idx: tab index
162
169
  :param column_idx: column index
163
170
  """
164
- tab = self.window.core.tabs.get_tab_by_index(idx, column_idx)
171
+ w = self.window
172
+ core = w.core
173
+ tabs_core = core.tabs
174
+
175
+ tab = tabs_core.get_tab_by_index(idx, column_idx)
165
176
  if tab is None:
166
177
  self.appended = False
167
178
  return
@@ -171,9 +182,9 @@ class Tabs:
171
182
  if tab.type == Tab.TAB_CHAT:
172
183
  self.current = idx
173
184
  if self.create_new_on_tab:
174
- meta = self.window.controller.ctx.new() # new context
185
+ meta = w.controller.ctx.new()
175
186
  if meta is not None:
176
- self.window.controller.ctx.load(meta.id) # reload
187
+ w.controller.ctx.load(meta.id)
177
188
  self.create_new_on_tab = True
178
189
 
179
190
  prev_tab = self.current
@@ -181,33 +192,31 @@ class Tabs:
181
192
 
182
193
  self.current = idx
183
194
  self.column_idx = column_idx
184
- self.window.controller.ui.mode.update()
185
- self.window.controller.ui.vision.update()
195
+ w.controller.ui.mode.update()
196
+ w.controller.ui.vision.update()
186
197
 
187
- # check type
188
198
  if tab.type == Tab.TAB_NOTEPAD:
189
- self.window.controller.notepad.opened_once = True
190
- self.window.controller.notepad.on_open(idx, column_idx)
199
+ w.controller.notepad.opened_once = True
200
+ w.controller.notepad.on_open(idx, column_idx)
191
201
  elif tab.type == Tab.TAB_CHAT:
192
- # get meta for selected tab, if not loaded yet then append meta here
193
202
  meta_id = tab.data_id
194
203
  if meta_id is None:
195
- meta_id = self.window.core.ctx.output.prepare_meta(tab)
196
- meta = self.window.core.ctx.get_meta_by_id(meta_id)
204
+ meta_id = core.ctx.output.prepare_meta(tab)
205
+ meta = core.ctx.get_meta_by_id(meta_id)
197
206
  if meta is not None:
198
- self.window.controller.ctx.load(meta.id) # reload renderer
207
+ w.controller.ctx.load(meta.id)
199
208
  elif tab.type == Tab.TAB_TOOL_PAINTER:
200
- if self.window.core.config.get('vision.capture.enabled'):
201
- self.window.controller.camera.enable_capture()
209
+ if core.config.get('vision.capture.enabled'):
210
+ w.controller.camera.enable_capture()
202
211
  elif tab.type == Tab.TAB_TOOL_CALENDAR:
203
- self.window.controller.calendar.update()
204
- self.window.controller.calendar.update_ctx_counters()
212
+ w.controller.calendar.update()
213
+ w.controller.calendar.update_ctx_counters()
205
214
 
206
215
  if prev_tab != idx or prev_column != column_idx:
207
- self.window.dispatch(AppEvent(AppEvent.TAB_SELECTED)) # app event
216
+ w.dispatch(AppEvent(AppEvent.TAB_SELECTED))
208
217
 
209
218
  self.on_changed()
210
- self.window.controller.ui.update()
219
+ w.controller.ui.update()
211
220
  self.update_current()
212
221
  self.debug()
213
222
 
@@ -251,7 +260,6 @@ class Tabs:
251
260
  :return: tab type
252
261
  """
253
262
  tab = self.window.core.tabs.get_tab_by_index(self.get_current_idx(), self.column_idx)
254
-
255
263
  if tab is None:
256
264
  return None
257
265
  return tab.type
@@ -324,22 +332,20 @@ class Tabs:
324
332
  """Column changed event"""
325
333
  if self.locked:
326
334
  return
327
- tabs = self.window.ui.layout.get_tabs_by_idx(self.column_idx)
335
+ layout = self.window.ui.layout
336
+ tabs = layout.get_tabs_by_idx(self.column_idx)
328
337
  tabs.set_active(True)
329
338
 
330
- if self.column_idx == 0:
331
- second_tabs = self.window.ui.layout.get_tabs_by_idx(1)
332
- else:
333
- second_tabs = self.window.ui.layout.get_tabs_by_idx(0)
339
+ second_tabs = layout.get_tabs_by_idx(1 if self.column_idx == 0 else 0)
334
340
  second_tabs.set_active(False)
341
+
335
342
  idx = tabs.currentIndex()
336
343
  self.current = idx
337
344
  tab = self.window.core.tabs.get_tab_by_index(self.current, self.column_idx)
338
345
  if tab is None:
339
346
  return
340
347
 
341
- # redraw second tab if not loaded yet
342
- if tab.type == Tab.TAB_CHAT and self.column_idx == 1 and not tab.loaded:
348
+ if tab.type == Tab.TAB_CHAT and self.column_idx == 1 and not getattr(tab, "loaded", False):
343
349
  meta = self.window.core.ctx.get_meta_by_id(tab.data_id)
344
350
  if meta is not None:
345
351
  self.window.controller.ctx.load(meta.id)
@@ -416,14 +422,14 @@ class Tabs:
416
422
  return
417
423
 
418
424
  previous_current = self.current
419
- idx_after = None # <--- next tab index after close to switch to
425
+ idx_after = None
420
426
  if previous_current != idx and self.column_idx == column_idx:
421
427
  idx_after = previous_current
422
428
  if idx_after > idx:
423
- idx_after -= 1 # if current is after closed tab, idx will be shifted
429
+ idx_after -= 1
424
430
 
425
431
  if idx_after is None:
426
- idx_after = self.get_after_close_idx(idx) # find next tab index after close
432
+ idx_after = self.get_after_close_idx(idx)
427
433
 
428
434
  self.window.core.tabs.remove_tab_by_idx(idx, column_idx)
429
435
  if idx_after is not None:
@@ -496,21 +502,21 @@ class Tabs:
496
502
  """Switch to next tab"""
497
503
  tabs = self.window.ui.layout.get_active_tabs()
498
504
  current = tabs.currentIndex()
499
- all = len(tabs.children())
500
- next = current + 1
501
- if next >= all:
502
- next = 0
503
- self.switch_tab_by_idx(next)
505
+ total = tabs.count()
506
+ nxt = current + 1
507
+ if nxt >= total:
508
+ nxt = 0
509
+ self.switch_tab_by_idx(nxt)
504
510
 
505
511
  def prev_tab(self):
506
512
  """Switch to previous tab"""
507
513
  tabs = self.window.ui.layout.get_active_tabs()
508
514
  current = tabs.currentIndex()
509
- all = len(tabs.children())
510
- prev = current - 1
511
- if prev < 0:
512
- prev = all - 1
513
- self.switch_tab_by_idx(prev)
515
+ total = tabs.count()
516
+ prv = current - 1
517
+ if prv < 0:
518
+ prv = total - 1
519
+ self.switch_tab_by_idx(prv)
514
520
 
515
521
  def switch_tab(self, type: int):
516
522
  """
@@ -560,14 +566,13 @@ class Tabs:
560
566
  if tab.type in self.window.core.tabs.titles:
561
567
  title = trans(self.window.core.tabs.titles[tab.type])
562
568
 
563
- # if more than 1 with this type then attach position info
564
569
  num = self.window.core.tabs.count_by_type(tab.type)
565
570
  if num > 1:
566
571
  order = self.window.core.tabs.get_order_by_idx_and_type(tab.idx, tab.type)
567
572
  if order != -1:
568
- title += " #" + str(order)
573
+ title += f" #{order}"
569
574
  if tab.tooltip is not None and tab.tooltip != "":
570
- title += " - " + tab.tooltip
575
+ title += f" - {tab.tooltip}"
571
576
  return title
572
577
 
573
578
  def update_tooltip(self, tooltip: str):
@@ -591,11 +596,9 @@ class Tabs:
591
596
  :param idx: tab idx
592
597
  :param column_idx: column idx
593
598
  """
594
- # get tab
595
599
  tab = self.window.core.tabs.get_tab_by_index(idx, column_idx)
596
600
  if tab is None:
597
601
  return
598
- # set dialog and show
599
602
  self.window.ui.dialog['rename'].id = 'tab'
600
603
  self.window.ui.dialog['rename'].input.setText(tab.title)
601
604
  self.window.ui.dialog['rename'].current = idx
@@ -638,14 +641,13 @@ class Tabs:
638
641
  :param idx: tab idx
639
642
  :param title: new title
640
643
  """
641
- # check if current tab is chat
642
644
  if self.get_current_type() != Tab.TAB_CHAT:
643
645
  return
644
646
  tabs = self.window.ui.layout.get_active_tabs()
645
647
  tooltip = title
646
648
  tabs.setTabToolTip(idx, tooltip)
647
649
  if len(title) > self.TAB_CHAT_MAX_CHARS:
648
- title = title[:self.TAB_CHAT_MAX_CHARS] + '...' # truncate to max 8 chars
650
+ title = title[:self.TAB_CHAT_MAX_CHARS] + '...'
649
651
  self.window.core.tabs.update_title(idx, title, tooltip)
650
652
  self.debug()
651
653
 
@@ -663,7 +665,6 @@ class Tabs:
663
665
 
664
666
  :param meta: context meta
665
667
  """
666
- # get current tab
667
668
  tab = self.get_current_tab()
668
669
  if tab is not None and tab.type == Tab.TAB_CHAT:
669
670
  tab.data_id = meta.id
@@ -686,7 +687,6 @@ class Tabs:
686
687
 
687
688
  :param column_idx: column index
688
689
  """
689
- # append at the end of column
690
690
  idx = self.window.core.tabs.get_max_idx_by_column(column_idx)
691
691
  if idx == -1:
692
692
  idx = 0
@@ -703,14 +703,8 @@ class Tabs:
703
703
  if not data:
704
704
  self.switch_tab_by_idx(0, 0)
705
705
  return
706
-
707
- # reverse order, second column is first
708
- data = dict(reversed(list(data.items())))
709
- for col_idx in data:
710
- tab_idx = data[col_idx]
706
+ for col_idx, tab_idx in reversed(list(data.items())):
711
707
  self.switch_tab_by_idx(int(tab_idx), int(col_idx))
712
-
713
- # set default column to 0
714
708
  self.column_idx = 0
715
709
  self.on_column_changed()
716
710
  self.debug()
@@ -732,10 +726,8 @@ class Tabs:
732
726
  tab = self.window.core.tabs.get_tab_by_index(idx, column_idx)
733
727
  self.window.core.tabs.move_tab(tab, new_column_idx)
734
728
  self.locked = False
735
- # switch to new column
736
729
  self.column_idx = new_column_idx
737
730
  self.on_column_changed()
738
- # switch to new tab
739
731
  self.switch_tab_by_idx(tab.idx, new_column_idx)
740
732
  self.debug()
741
733
 
@@ -835,10 +827,8 @@ class Tabs:
835
827
  """Switch to first chat tab"""
836
828
  if self.is_current_by_type(Tab.TAB_CHAT):
837
829
  return
838
- # abort if active tab is chat
839
830
  if self.get_current_type() == Tab.TAB_CHAT:
840
831
  return
841
- # find first chat tab
842
832
  for col in self.col:
843
833
  pid = self.col[col]
844
834
  tab = self.window.core.tabs.get_tab_by_pid(pid)
@@ -861,10 +851,7 @@ class Tabs:
861
851
  :param title: new tab name (optional, for chat tab)
862
852
  :param meta: context meta (optional, for chat tab)
863
853
  """
864
- # try to focus tab
865
854
  if self.get_current_type() != type:
866
-
867
- # find the closest tab in current column (on left side)
868
855
  current = self.get_current_tab()
869
856
  exists = False
870
857
  if current:
@@ -876,11 +863,9 @@ class Tabs:
876
863
  if exists:
877
864
  tab = self.window.core.tabs.get_tab_by_index(idx, column_idx)
878
865
  else:
879
- # if not exists in current col, then find first idx in any column
880
866
  tab = self.window.core.tabs.get_first_by_type(type)
881
867
 
882
868
  if tab:
883
- # if tab is found in current column, switch to it
884
869
  tabs = self.window.ui.layout.get_tabs_by_idx(tab.column_idx)
885
870
  if tabs:
886
871
  idx = tab.idx
@@ -897,20 +882,16 @@ class Tabs:
897
882
  self.on_column_focus(tab.column_idx)
898
883
  tabs.setCurrentIndex(idx)
899
884
  else:
900
- # if not found in current column, then check in second column
901
885
  second_column_idx = 1 if self.column_idx == 0 else 0
902
- # get current tab from second column
903
886
  tabs = self.window.ui.layout.get_tabs_by_idx(second_column_idx)
904
887
  second_tabs_idx = tabs.currentIndex()
905
888
  second_tab = self.window.core.tabs.get_tab_by_index(second_tabs_idx, second_column_idx)
906
889
  if second_tab is not None and second_tab.type == type:
907
- # switch to second column
908
890
  self.on_column_focus(second_column_idx)
909
891
  tabs.setCurrentIndex(second_tabs_idx)
910
892
  if meta:
911
893
  QTimer.singleShot(100, lambda: self.window.controller.ctx.load(meta.id))
912
894
 
913
- # if second and split screen disabled, then enable it
914
895
  if tab and tab.column_idx == 1:
915
896
  if not self.is_split_screen_enabled():
916
897
  self.enable_split_screen(update_switch=True)
@@ -925,7 +906,6 @@ class Tabs:
925
906
  """
926
907
  return self.window.core.config.get("layout.split", False)
927
908
 
928
-
929
909
  def on_split_screen_changed(self, state: bool):
930
910
  """
931
911
  On split screen mode changed
@@ -960,7 +940,6 @@ class Tabs:
960
940
  Disable split screen mode
961
941
  """
962
942
  self.window.ui.splitters['columns'].setSizes([1, 0])
963
- # switch to first column
964
943
  self.column_idx = 0
965
944
  self.on_column_changed()
966
945
  self.window.core.config.set("layout.split", False)
@@ -1,4 +1,4 @@
1
- # !/usr/bin/env python3
1
+ #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
3
  # ================================================== #
4
4
  # This file is a part of PYGPT package #
@@ -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.26 18:00:00 #
9
+ # Updated Date: 2025.08.15 03:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Optional
@@ -45,6 +45,11 @@ class UI:
45
45
  self.splitter_output_size_input = None
46
46
  self.splitter_output_size_files = None
47
47
 
48
+ self._last_ctx_string = None
49
+ self._last_input_string = None
50
+ self._last_chat_model = None
51
+ self._last_chat_label = None
52
+
48
53
  def setup(self):
49
54
  """Setup UI"""
50
55
  self.update_font_size()
@@ -53,23 +58,11 @@ class UI:
53
58
 
54
59
  def update(self):
55
60
  """Update all elements"""
56
-
57
- # update mode, models and presets list
58
61
  self.update_toolbox()
59
-
60
- # update chat label
61
62
  self.update_chat_label()
62
-
63
- # show / hide widgets
64
63
  self.mode.update()
65
-
66
- # update token counters
67
64
  self.update_tokens()
68
-
69
- # update vision
70
65
  self.vision.update()
71
-
72
- # agent status
73
66
  self.window.controller.agent.legacy.update()
74
67
 
75
68
  def get_colors(self) -> dict:
@@ -100,9 +93,10 @@ class UI:
100
93
  env = "mac"
101
94
  else:
102
95
  env = "linux"
103
- index = self.window.ui.nodes["computer_env"].findData(env)
104
- if index != -1:
105
- self.window.ui.nodes["computer_env"].setCurrentIndex(index)
96
+ node = self.window.ui.nodes["computer_env"]
97
+ index = node.findData(env)
98
+ if index != -1 and node.currentIndex() != index:
99
+ node.setCurrentIndex(index)
106
100
 
107
101
  def on_computer_env_changed(self, env: str):
108
102
  """
@@ -110,16 +104,19 @@ class UI:
110
104
 
111
105
  :param env: selected environment
112
106
  """
113
- self.window.core.config.set("remote_tools.computer_use.env", env)
114
- self.window.core.config.save()
107
+ cfg = self.window.core.config
108
+ if cfg.get("remote_tools.computer_use.env") != env:
109
+ cfg.set("remote_tools.computer_use.env", env)
110
+ cfg.save()
115
111
 
116
112
  def update_toolbox(self):
117
113
  """Update toolbox"""
118
- self.window.controller.mode.update_mode()
119
- self.window.controller.model.update()
120
- self.window.controller.presets.refresh()
121
- self.window.controller.assistant.refresh()
122
- self.window.controller.idx.refresh()
114
+ ctrl = self.window.controller
115
+ ctrl.mode.update_mode()
116
+ ctrl.model.update()
117
+ ctrl.presets.refresh()
118
+ ctrl.assistant.refresh()
119
+ ctrl.idx.refresh()
123
120
 
124
121
  def format_tokens(self, num: int) -> str:
125
122
  """
@@ -130,41 +127,31 @@ class UI:
130
127
  num = int(num)
131
128
  if num >= 1_000_000:
132
129
  return f"{num // 1_000_000}M"
133
- elif num >= 1_000:
130
+ if num >= 1_000:
134
131
  return f"{num // 1_000}k"
135
- else:
136
- return str(num)
132
+ return str(num)
137
133
 
138
134
  def update_tokens(self):
139
135
  """Update tokens counter in real-time"""
140
- prompt = str(self.window.ui.nodes['input'].toPlainText().strip())
136
+ ui_nodes = self.window.ui.nodes
137
+ prompt = ui_nodes['input'].toPlainText().strip()
141
138
  input_tokens, system_tokens, extra_tokens, ctx_tokens, ctx_len, ctx_len_all, \
142
139
  sum_tokens, max_current, threshold = self.window.core.tokens.get_current(prompt)
143
140
  attachments_tokens = self.window.controller.chat.attachment.get_current_tokens()
144
141
  sum_tokens += attachments_tokens
145
142
 
146
- # ctx tokens
147
- ctx_string = "{} / {} - {} {}".format(
148
- ctx_len,
149
- ctx_len_all,
150
- ctx_tokens,
151
- trans('ctx.tokens')
152
- )
153
- self.window.ui.nodes['prompt.context'].setText(ctx_string)
143
+ ctx_string = f"{ctx_len} / {ctx_len_all} - {ctx_tokens} {trans('ctx.tokens')}"
144
+ if ctx_string != self._last_ctx_string:
145
+ ui_nodes['prompt.context'].setText(ctx_string)
146
+ self._last_ctx_string = ctx_string
154
147
 
155
148
  parsed_sum = self.format_tokens(sum_tokens)
156
149
  parsed_max_current = self.format_tokens(max_current)
157
150
 
158
- input_string = "{} + {} + {} + {} + {} = {} / {}".format(
159
- input_tokens,
160
- system_tokens,
161
- ctx_tokens,
162
- extra_tokens,
163
- attachments_tokens,
164
- parsed_sum,
165
- parsed_max_current
166
- )
167
- self.window.ui.nodes['input.counter'].setText(input_string)
151
+ input_string = f"{input_tokens} + {system_tokens} + {ctx_tokens} + {extra_tokens} + {attachments_tokens} = {parsed_sum} / {parsed_max_current}"
152
+ if input_string != self._last_input_string:
153
+ ui_nodes['input.counter'].setText(input_string)
154
+ self._last_input_string = input_string
168
155
 
169
156
  def store_state(self):
170
157
  """Store UI state"""
@@ -177,11 +164,10 @@ class UI:
177
164
  def update_chat_label(self):
178
165
  """Update chat label"""
179
166
  model = self.window.core.config.get('model')
180
- if model is None or model == "":
181
- model_str = ""
182
- else:
183
- model_str = str(model)
184
- self.window.ui.nodes['chat.model'].setText(model_str)
167
+ model_str = "" if not model else str(model)
168
+ if model_str != self._last_chat_model:
169
+ self.window.ui.nodes['chat.model'].setText(model_str)
170
+ self._last_chat_model = model_str
185
171
 
186
172
  def update_ctx_label(self, label: Optional[str] = None):
187
173
  """
@@ -193,19 +179,24 @@ class UI:
193
179
  allowed = self.window.core.ctx.is_allowed_for_mode(mode)
194
180
  if label is None:
195
181
  label = ''
196
-
197
- # add (+) if allowed appending data to this context
198
182
  if allowed:
199
183
  label += ' (+)'
200
- self.window.ui.nodes['chat.label'].setText(str(label))
184
+ label_str = str(label)
185
+ if label_str != self._last_chat_label:
186
+ self.window.ui.nodes['chat.label'].setText(label_str)
187
+ self._last_chat_label = label_str
201
188
 
202
189
  def show_global_stop(self):
203
190
  """Show global stop button"""
204
- self.window.ui.nodes['global.stop'].setVisible(True)
191
+ node = self.window.ui.nodes['global.stop']
192
+ if not node.isVisible():
193
+ node.setVisible(True)
205
194
 
206
195
  def hide_global_stop(self):
207
196
  """Hide global stop button"""
208
- self.window.ui.nodes['global.stop'].setVisible(False)
197
+ node = self.window.ui.nodes['global.stop']
198
+ if node.isVisible():
199
+ node.setVisible(False)
209
200
 
210
201
  def on_global_stop(self):
211
202
  """Global stop button action"""
@@ -1,4 +1,4 @@
1
- # !/usr/bin/env python3
1
+ #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
3
  # ================================================== #
4
4
  # This file is a part of PYGPT package #
@@ -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.30 00:00:00 #
9
+ # Updated Date: 2025.08.15 03:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from pygpt_net.core.types import (
@@ -31,7 +31,7 @@ class Vision:
31
31
  """
32
32
  self.window = window
33
33
 
34
- def has_vision(self):
34
+ def has_vision(self) -> bool:
35
35
  """
36
36
  Check if vision is available
37
37
 
@@ -44,30 +44,34 @@ class Vision:
44
44
  return True
45
45
  if self.window.controller.plugins.is_type_enabled('vision'):
46
46
  return True
47
- if self.is_vision_model() and mode in [
47
+ if self.is_vision_model() and mode in (
48
48
  MODE_CHAT,
49
49
  MODE_LLAMA_INDEX,
50
50
  MODE_AGENT,
51
51
  MODE_AGENT_OPENAI,
52
52
  MODE_RESEARCH,
53
53
  MODE_EXPERT,
54
- ]:
54
+ ):
55
55
  return True
56
56
  return False
57
57
 
58
- def update(self):
58
+ def update(self) -> None:
59
59
  """Update vision options"""
60
- if self.window.controller.painter.is_active():
61
- self.window.controller.camera.setup()
62
- self.window.controller.camera.show_camera()
60
+ ctrl = self.window.controller
61
+ camera = ctrl.camera
62
+
63
+ if ctrl.painter.is_active():
64
+ camera.setup()
65
+ camera.show_camera()
66
+ return
67
+
68
+ if self.has_vision():
69
+ camera.setup()
70
+ camera.show_camera()
71
+ ctrl.chat.vision.show_inline()
63
72
  else:
64
- if self.has_vision():
65
- self.window.controller.camera.setup()
66
- self.window.controller.camera.show_camera()
67
- self.window.controller.chat.vision.show_inline()
68
- else:
69
- self.window.controller.camera.hide_camera()
70
- self.window.controller.chat.vision.hide_inline()
73
+ camera.hide_camera()
74
+ ctrl.chat.vision.hide_inline()
71
75
 
72
76
  def is_vision_model(self) -> bool:
73
77
  """
@@ -76,10 +80,7 @@ class Vision:
76
80
  :return: True if vision model
77
81
  """
78
82
  model_id = self.window.core.config.get("model")
79
- if model_id is not None:
80
- if self.window.core.models.has(model_id):
81
- model = self.window.core.models.get(model_id)
82
- if model.is_image_input():
83
- return True
84
- return False
85
-
83
+ if not model_id:
84
+ return False
85
+ models = self.window.core.models
86
+ return models.has(model_id) and models.get(model_id).is_image_input()