pygpt-net 2.6.60__py3-none-any.whl → 2.6.62__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 (87) hide show
  1. pygpt_net/CHANGELOG.txt +14 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/chat/common.py +115 -6
  4. pygpt_net/controller/chat/input.py +4 -1
  5. pygpt_net/controller/chat/response.py +8 -2
  6. pygpt_net/controller/presets/presets.py +121 -6
  7. pygpt_net/controller/settings/editor.py +0 -15
  8. pygpt_net/controller/settings/profile.py +16 -4
  9. pygpt_net/controller/settings/workdir.py +30 -5
  10. pygpt_net/controller/theme/common.py +4 -2
  11. pygpt_net/controller/theme/markdown.py +4 -7
  12. pygpt_net/controller/theme/theme.py +2 -1
  13. pygpt_net/controller/ui/ui.py +32 -7
  14. pygpt_net/core/agents/custom/__init__.py +7 -1
  15. pygpt_net/core/agents/custom/llama_index/factory.py +17 -6
  16. pygpt_net/core/agents/custom/llama_index/runner.py +52 -4
  17. pygpt_net/core/agents/custom/llama_index/utils.py +12 -1
  18. pygpt_net/core/agents/custom/router.py +45 -6
  19. pygpt_net/core/agents/custom/runner.py +11 -5
  20. pygpt_net/core/agents/custom/schema.py +3 -1
  21. pygpt_net/core/agents/custom/utils.py +13 -1
  22. pygpt_net/core/agents/runners/llama_workflow.py +65 -5
  23. pygpt_net/core/agents/runners/openai_workflow.py +2 -1
  24. pygpt_net/core/db/viewer.py +11 -5
  25. pygpt_net/core/node_editor/graph.py +18 -9
  26. pygpt_net/core/node_editor/models.py +9 -2
  27. pygpt_net/core/node_editor/types.py +15 -1
  28. pygpt_net/core/presets/presets.py +216 -29
  29. pygpt_net/core/render/markdown/parser.py +0 -2
  30. pygpt_net/core/render/web/renderer.py +76 -11
  31. pygpt_net/data/config/config.json +5 -6
  32. pygpt_net/data/config/models.json +3 -3
  33. pygpt_net/data/config/settings.json +2 -38
  34. pygpt_net/data/css/style.dark.css +18 -0
  35. pygpt_net/data/css/style.light.css +20 -1
  36. pygpt_net/data/locale/locale.de.ini +66 -1
  37. pygpt_net/data/locale/locale.en.ini +64 -3
  38. pygpt_net/data/locale/locale.es.ini +66 -1
  39. pygpt_net/data/locale/locale.fr.ini +66 -1
  40. pygpt_net/data/locale/locale.it.ini +66 -1
  41. pygpt_net/data/locale/locale.pl.ini +67 -2
  42. pygpt_net/data/locale/locale.uk.ini +66 -1
  43. pygpt_net/data/locale/locale.zh.ini +66 -1
  44. pygpt_net/data/locale/plugin.cmd_system.en.ini +62 -66
  45. pygpt_net/item/ctx.py +23 -1
  46. pygpt_net/provider/agents/llama_index/flow_from_schema.py +2 -2
  47. pygpt_net/provider/agents/llama_index/workflow/codeact.py +9 -6
  48. pygpt_net/provider/agents/llama_index/workflow/openai.py +38 -11
  49. pygpt_net/provider/agents/llama_index/workflow/planner.py +36 -16
  50. pygpt_net/provider/agents/llama_index/workflow/supervisor.py +60 -10
  51. pygpt_net/provider/agents/openai/agent.py +3 -1
  52. pygpt_net/provider/agents/openai/agent_b2b.py +13 -9
  53. pygpt_net/provider/agents/openai/agent_planner.py +6 -2
  54. pygpt_net/provider/agents/openai/agent_with_experts.py +4 -1
  55. pygpt_net/provider/agents/openai/agent_with_experts_feedback.py +4 -2
  56. pygpt_net/provider/agents/openai/agent_with_feedback.py +4 -2
  57. pygpt_net/provider/agents/openai/evolve.py +6 -2
  58. pygpt_net/provider/agents/openai/supervisor.py +3 -1
  59. pygpt_net/provider/api/openai/agents/response.py +1 -0
  60. pygpt_net/provider/core/config/patch.py +18 -1
  61. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +0 -6
  62. pygpt_net/tools/agent_builder/tool.py +48 -26
  63. pygpt_net/tools/agent_builder/ui/dialogs.py +36 -28
  64. pygpt_net/ui/__init__.py +2 -4
  65. pygpt_net/ui/dialog/about.py +58 -38
  66. pygpt_net/ui/dialog/db.py +142 -3
  67. pygpt_net/ui/dialog/preset.py +47 -8
  68. pygpt_net/ui/layout/toolbox/presets.py +64 -16
  69. pygpt_net/ui/main.py +2 -2
  70. pygpt_net/ui/widget/dialog/confirm.py +27 -3
  71. pygpt_net/ui/widget/dialog/db.py +0 -0
  72. pygpt_net/ui/widget/draw/painter.py +90 -1
  73. pygpt_net/ui/widget/lists/preset.py +908 -60
  74. pygpt_net/ui/widget/node_editor/command.py +10 -10
  75. pygpt_net/ui/widget/node_editor/config.py +157 -0
  76. pygpt_net/ui/widget/node_editor/editor.py +223 -153
  77. pygpt_net/ui/widget/node_editor/item.py +12 -11
  78. pygpt_net/ui/widget/node_editor/node.py +246 -13
  79. pygpt_net/ui/widget/node_editor/view.py +179 -63
  80. pygpt_net/ui/widget/tabs/output.py +1 -1
  81. pygpt_net/ui/widget/textarea/input.py +157 -23
  82. pygpt_net/utils.py +114 -2
  83. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/METADATA +26 -100
  84. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/RECORD +86 -85
  85. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/LICENSE +0 -0
  86. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/WHEEL +0 -0
  87. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/entry_points.txt +0 -0
pygpt_net/CHANGELOG.txt CHANGED
@@ -1,3 +1,17 @@
1
+ 2.6.62 (2025-09-26)
2
+
3
+ - Enhanced agent workflow execution.
4
+ - Improved preset list handling by adding a drop field indicator and fixing auto-scroll.
5
+ - Added middle-mouse button panning to Painter.
6
+ - Added an input character counter.
7
+
8
+ 2.6.61 (2025-09-26)
9
+
10
+ - Enhanced the agents node editor, custom agent flow, and instruction following.
11
+ - Added drag-and-drop and reordering functionality to the presets list.
12
+ - Added statistics for response tokens, including time elapsed and tokens per second.
13
+ - Improved UI/UX.
14
+
1
15
  2.6.60 (2025-09-25)
2
16
 
3
17
  - Added a new tool: Agents Builder - allowing visual design of agent workflows using nodes - available in Tools -> Agents Builder (beta).
pygpt_net/__init__.py CHANGED
@@ -6,15 +6,15 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.25 00:00:00 #
9
+ # Updated Date: 2025.09.26 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  __author__ = "Marcin Szczygliński"
13
13
  __copyright__ = "Copyright 2025, Marcin Szczygliński"
14
14
  __credits__ = ["Marcin Szczygliński"]
15
15
  __license__ = "MIT"
16
- __version__ = "2.6.60"
17
- __build__ = "2025-09-25"
16
+ __version__ = "2.6.62"
17
+ __build__ = "2025-09-26"
18
18
  __maintainer__ = "Marcin Szczygliński"
19
19
  __github__ = "https://github.com/szczyglis-dev/py-gpt"
20
20
  __report__ = "https://github.com/szczyglis-dev/py-gpt/issues"
@@ -6,10 +6,12 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.01 23:00:00 #
9
+ # Updated Date: 2025.09.25 12:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
13
+ from time import perf_counter
14
+ from typing import Callable, Optional
13
15
 
14
16
  from PySide6.QtGui import QTextCursor
15
17
  from PySide6.QtWidgets import QFileDialog, QApplication
@@ -18,7 +20,7 @@ from pygpt_net.core.events import Event, AppEvent, RenderEvent, KernelEvent
18
20
  from pygpt_net.core.types import MODE_ASSISTANT, MODE_AUDIO
19
21
  from pygpt_net.item.ctx import CtxItem
20
22
  from pygpt_net.item.model import ModelItem
21
- from pygpt_net.utils import trans
23
+ from pygpt_net.utils import trans, short_num
22
24
 
23
25
 
24
26
  class Common:
@@ -30,6 +32,12 @@ class Common:
30
32
  """
31
33
  self.window = window
32
34
  self.initialized = False
35
+ # Counter (seconds, float) – current/last measured time
36
+ self.counter: float = 0.0
37
+ # Private start timestamp
38
+ self._t0: Optional[float] = None
39
+ # Optional number-shortening function (e.g., 10000 -> "10k"); if None, a fallback is used
40
+ self._shortener = None
33
41
 
34
42
  def setup(self):
35
43
  """Set up UI"""
@@ -310,6 +318,7 @@ class Common:
310
318
  dispatch(AppEvent(AppEvent.INPUT_STOPPED)) # app event
311
319
 
312
320
  self.stop_client() # stop clients
321
+ self.reset_counter()
313
322
 
314
323
  def stop_client(self):
315
324
  """Stop all clients"""
@@ -457,15 +466,115 @@ class Common:
457
466
  f.write(str(text).strip())
458
467
  self.window.update_status(f"{trans('status.saved')}: {os.path.basename(file_name)}")
459
468
 
469
+ # --- Timer control ---
470
+
471
+ def start_counter(self) -> None:
472
+ """Start the timer – call when send the request."""
473
+ self._t0 = perf_counter()
474
+
475
+ def stop_counter(self) -> float:
476
+ """
477
+ Stop the timer. Returns elapsed seconds and stores it in self.counter.
478
+ """
479
+ if self._t0 is None:
480
+ raise RuntimeError("Timer was not started (start_counter()).")
481
+ self.counter = perf_counter() - self._t0
482
+ self._t0 = None
483
+ return self.counter
484
+
485
+ def get_counter(self) -> float:
486
+ """
487
+ Current elapsed time (if running) or last measured time (in seconds).
488
+ """
489
+ if self._t0 is not None:
490
+ return perf_counter() - self._t0
491
+ return self.counter
492
+
493
+ def reset_counter(self) -> None:
494
+ """Reset the timer."""
495
+ self._t0 = None
496
+ self.counter = 0.0
497
+
498
+ # --- Calculation and formatting ---
499
+
500
+ def tokens_per_second(self, tokens_generated: int, seconds: Optional[float] = None) -> float:
501
+ """
502
+ Average tokens/s for the given token count and time (defaults to current/last).
503
+ """
504
+ sec = self.get_counter() if seconds is None else max(float(seconds), 0.0)
505
+ if sec <= 0.0:
506
+ return 0.0
507
+ return float(tokens_generated) / sec
508
+
509
+ def format_duration(self, seconds: Optional[float] = None, max_units: int = 2) -> str:
510
+ """
511
+ Pretty duration: e.g., 800ms, 12s, 1m 12s, 3h 5m, 1d 2h (by default up to 2 units).
512
+ """
513
+ sec = self.get_counter() if seconds is None else max(float(seconds), 0.0)
514
+
515
+ if sec < 1.0:
516
+ return f"{int(round(sec * 1000)):d}ms"
517
+
518
+ total = int(sec) # floor to whole seconds
519
+ days, rem = divmod(total, 86400)
520
+ hours, rem = divmod(rem, 3600)
521
+ minutes, seconds = divmod(rem, 60)
522
+
523
+ parts = []
524
+ if days:
525
+ parts.append(f"{days}d")
526
+ if hours:
527
+ parts.append(f"{hours}h")
528
+ if minutes:
529
+ parts.append(f"{minutes}m")
530
+ if seconds or not parts:
531
+ parts.append(f"{seconds}s")
532
+
533
+ return " ".join(parts[:max_units])
534
+
535
+ def _fallback_shorten(self, value: float) -> str:
536
+ """
537
+ Simple fallback shortener (e.g., 15300 -> '15.3k').
538
+ """
539
+ n = abs(value)
540
+ sign = "-" if value < 0 else ""
541
+ if n < 1_000:
542
+ return f"{sign}{int(round(n))}"
543
+ elif n < 1_000_000:
544
+ v = n / 1_000
545
+ s = f"{v:.1f}".rstrip("0").rstrip(".")
546
+ return f"{sign}{s}k"
547
+ elif n < 1_000_000_000:
548
+ v = n / 1_000_000
549
+ s = f"{v:.1f}".rstrip("0").rstrip(".")
550
+ return f"{sign}{s}M"
551
+ else:
552
+ v = n / 1_000_000_000
553
+ s = f"{v:.1f}".rstrip("0").rstrip(".")
554
+ return f"{sign}{s}B"
555
+
556
+ def format_stats(self, tokens_generated: int) -> str:
557
+ """
558
+ Returns a string in the format: "<X> tokens/s - <duration>", e.g., "15k tokens/s - 1m 12s".
559
+ """
560
+ tps = self.tokens_per_second(tokens_generated)
561
+ if self._shortener:
562
+ tps_str = self._shortener(tps if isinstance(tps, (int, float)) else float(tps))
563
+ else:
564
+ tps_str = self._fallback_shorten(tps)
565
+ duration = self.format_duration()
566
+ return f"{tps_str} tokens/s - {duration}"
567
+
460
568
  def show_response_tokens(self, ctx: CtxItem):
461
569
  """
462
570
  Update response tokens
463
571
 
464
572
  :param ctx: CtxItem
465
573
  """
466
- extra_data = ""
467
- if ctx.is_vision:
468
- extra_data = " (VISION)"
574
+ stats = ""
575
+ if ctx.output_tokens > 0 and self.get_counter() > 0:
576
+ stats = f" | {self.format_stats(ctx.output_tokens)}"
577
+ self.reset_counter()
469
578
  self.window.update_status(
470
- f"{trans('status.tokens')}: {ctx.input_tokens} + {ctx.output_tokens} = {ctx.total_tokens}{extra_data}"
579
+ f"{trans('status.tokens')}: {short_num(ctx.input_tokens)} + {short_num(ctx.output_tokens)} = {short_num(ctx.total_tokens)}{stats}"
471
580
  )
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.23 15:00:00 #
9
+ # Updated Date: 2025.09.25 12:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Optional, Any, Dict
@@ -206,6 +206,9 @@ class Input:
206
206
  'mode': mode,
207
207
  }))
208
208
 
209
+ # start timer
210
+ self.window.controller.chat.common.start_counter()
211
+
209
212
  # send input to API
210
213
  if mode == MODE_IMAGE:
211
214
  controller.chat.image.send(
@@ -36,6 +36,7 @@ class Response:
36
36
  """
37
37
  super(Response, self).__init__()
38
38
  self.window = window
39
+ self.last_response_id = None
39
40
 
40
41
  def handle(
41
42
  self,
@@ -273,9 +274,14 @@ class Response:
273
274
  self.window.update_status(trans("status.agent.reasoning"))
274
275
  controller.chat.common.lock_input() # lock input, re-enable stop button
275
276
 
276
- # agent final response
277
+ # agent final response, with fix for async delayed finish (prevent multiple calls for the same response)
277
278
  if ctx.extra is not None and (isinstance(ctx.extra, dict) and "agent_finish" in ctx.extra):
278
- controller.agent.llama.on_finish(ctx) # evaluate response and continue if needed
279
+ consume = False
280
+ if self.last_response_id is None or self.last_response_id < ctx.id:
281
+ consume = True
282
+ self.last_response_id = ctx.id
283
+ if consume:
284
+ controller.agent.llama.on_finish(ctx) # evaluate response and continue if needed
279
285
 
280
286
  def end(
281
287
  self,
@@ -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.05 18:00:00 #
9
+ # Updated Date: 2025.09.26 03:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import re
@@ -24,12 +24,14 @@ 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
27
28
  from pygpt_net.core.events import AppEvent
28
29
  from pygpt_net.item.preset import PresetItem
29
30
  from pygpt_net.utils import trans
30
31
 
31
32
 
32
33
  _FILENAME_SANITIZE_RE = re.compile(r'[^a-zA-Z0-9_\-\.]')
34
+ # keep original validation (do not break other parts)
33
35
  _VALIDATE_FILENAME_RE = re.compile(r'[^\w\s\-\.]')
34
36
 
35
37
 
@@ -76,7 +78,7 @@ class Presets:
76
78
 
77
79
  def select(self, idx: int):
78
80
  """
79
- Select preset
81
+ Select preset by list index (legacy)
80
82
 
81
83
  :param idx: value of the list (row idx)
82
84
  """
@@ -94,6 +96,32 @@ class Presets:
94
96
  if editor_ctrl.opened and editor_ctrl.current != preset_id:
95
97
  self.editor.init(preset_id)
96
98
 
99
+ def select_by_id(self, preset_id: str):
100
+ """
101
+ Select preset by explicit ID (robust for DnD-ordered views).
102
+ """
103
+ if self.preset_change_locked():
104
+ return
105
+ if not preset_id:
106
+ return
107
+ w = self.window
108
+ mode = w.core.config.get('mode')
109
+ if not w.core.presets.has(mode, preset_id):
110
+ return
111
+ w.core.config.set("preset", preset_id)
112
+ if 'current_preset' not in w.core.config.data:
113
+ w.core.config.data['current_preset'] = {}
114
+ w.core.config.data['current_preset'][mode] = preset_id
115
+ self.select_model()
116
+ w.controller.ui.update()
117
+ w.controller.model.select_current()
118
+ w.dispatch(AppEvent(AppEvent.PRESET_SELECTED))
119
+ idx = w.core.presets.get_idx_by_id(mode, preset_id)
120
+ self.set_selected(idx)
121
+ editor_ctrl = w.controller.presets.editor
122
+ if editor_ctrl.opened and editor_ctrl.current != preset_id:
123
+ self.editor.init(preset_id)
124
+
97
125
  def get_current(self) -> Optional[PresetItem]:
98
126
  """
99
127
  Get current preset
@@ -584,6 +612,43 @@ class Presets:
584
612
  if mode == MODE_ASSISTANT:
585
613
  w.core.assistants.load()
586
614
 
615
+ def _nearest_id_after_delete(self, mode: str, idx: Optional[int], deleting_id: Optional[str]) -> Optional[str]:
616
+ """
617
+ Compute the nearest neighbor to select after deletion:
618
+ - Prefer the next item (below) if exists;
619
+ - Otherwise choose the previous one (above);
620
+ - Returns None when no neighbor can be determined.
621
+ This uses the current view order for the given mode, including pinned current.<mode> at index 0.
622
+ """
623
+ try:
624
+ w = self.window
625
+ data = w.core.presets.get_by_mode(mode) or {}
626
+ ids = list(data.keys())
627
+ if not ids:
628
+ return None
629
+
630
+ # If idx is invalid, try to resolve from deleting_id
631
+ if idx is None or idx < 0 or idx >= len(ids):
632
+ if deleting_id and deleting_id in ids:
633
+ idx = ids.index(deleting_id)
634
+ else:
635
+ return None
636
+
637
+ # Prefer below
638
+ if idx + 1 < len(ids):
639
+ cand = ids[idx + 1]
640
+ if cand and cand != deleting_id:
641
+ return cand
642
+
643
+ # Otherwise above
644
+ if idx - 1 >= 0:
645
+ cand = ids[idx - 1]
646
+ if cand and cand != deleting_id:
647
+ return cand
648
+ except Exception:
649
+ pass
650
+ return None
651
+
587
652
  def delete(
588
653
  self,
589
654
  idx: Optional[int] = None,
@@ -607,10 +672,31 @@ class Presets:
607
672
  msg=trans('confirm.preset.delete'),
608
673
  )
609
674
  return
610
- if preset_id == w.core.config.get('preset'):
611
- w.core.config.set('preset', None)
612
- w.ui.nodes['preset.prompt'].setPlainText("")
675
+
676
+ # Determine neighbor only if the deleted preset is currently active.
677
+ # This keeps API semantics untouched and prevents unexpected selection changes.
678
+ is_current = (preset_id == w.core.config.get('preset'))
679
+ target_id = None
680
+ if is_current:
681
+ target_id = self._nearest_id_after_delete(mode, idx, preset_id)
682
+
683
+ # Remove from core (also removes file when True)
613
684
  w.core.presets.remove(preset_id, True)
685
+
686
+ # When removing the active preset, jump to the nearest neighbor (below or above).
687
+ # If no neighbor can be determined, fall back to previous behavior (clear and let defaults apply).
688
+ if is_current:
689
+ if target_id and target_id in w.core.presets.items:
690
+ # Persist new selection in config (and keep current_preset mapping coherent)
691
+ w.core.config.set('preset', target_id)
692
+ if 'current_preset' not in w.core.config.data:
693
+ w.core.config.data['current_preset'] = {}
694
+ w.core.config.data['current_preset'][mode] = target_id
695
+ else:
696
+ # Fallback: clear selection to allow select_default() to pick the first available
697
+ w.core.config.set('preset', None)
698
+ w.ui.nodes['preset.prompt'].setPlainText("")
699
+
614
700
  self.refresh(no_scroll=True)
615
701
  w.update_status(trans('status.preset.deleted'))
616
702
 
@@ -715,4 +801,33 @@ class Presets:
715
801
 
716
802
  def clear_selected(self):
717
803
  """Clear selected list"""
718
- self.selected = []
804
+ self.selected = []
805
+
806
+ # ----------------------------
807
+ # Drag & drop ordering helpers
808
+ # ----------------------------
809
+
810
+ def persist_order_for_mode(self, mode: str, uuids: List[str]):
811
+ """
812
+ Persist new order (by UUIDs) for given mode.
813
+
814
+ The special '*' preset (current.<mode>) is not included here and always pinned at index 0.
815
+ """
816
+ w = self.window
817
+ cfg = w.core.config
818
+ order = cfg.get('presets_order') or {}
819
+ # Normalize to lists
820
+ if isinstance(order.get(mode), dict):
821
+ mapped = order.get(mode)
822
+ order[mode] = [mapped[k] for k in sorted(mapped.keys(), key=lambda x: int(x))]
823
+ order[mode] = [u for u in uuids if u]
824
+ cfg.set('presets_order', order)
825
+
826
+ def dnd_enabled(self) -> bool:
827
+ """
828
+ Check if drag and drop is globally enabled in config.
829
+ """
830
+ try:
831
+ return bool(self.window.core.config.get('presets.drag_and_drop.enabled'))
832
+ except Exception:
833
+ return False
@@ -51,11 +51,9 @@ class Editor:
51
51
  self.window.ui.add_hook("update.config.font_size.ctx", self.hook_update)
52
52
  self.window.ui.add_hook("update.config.font_size.toolbox", self.hook_update)
53
53
  self.window.ui.add_hook("update.config.zoom", self.hook_update)
54
- self.window.ui.add_hook("update.config.theme.markdown", self.hook_update)
55
54
  self.window.ui.add_hook("update.config.vision.capture.enabled", self.hook_update)
56
55
  self.window.ui.add_hook("update.config.vision.capture.auto", self.hook_update)
57
56
  self.window.ui.add_hook("update.config.ctx.records.limit", self.hook_update)
58
- self.window.ui.add_hook("update.config.ctx.convert_lists", self.hook_update)
59
57
  self.window.ui.add_hook("update.config.ctx.records.separators", self.hook_update)
60
58
  self.window.ui.add_hook("update.config.ctx.records.groups.separators", self.hook_update)
61
59
  self.window.ui.add_hook("update.config.ctx.records.pinned.separators", self.hook_update)
@@ -176,10 +174,6 @@ class Editor:
176
174
  value = self.window.core.config.get('theme.style')
177
175
  self.window.controller.theme.toggle_style(value)
178
176
 
179
- # convert lists
180
- if self.config_changed('ctx.convert_lists'):
181
- self.window.controller.ctx.refresh()
182
-
183
177
  # access: voice control
184
178
  if self.config_changed('access.voice_control'):
185
179
  self.window.controller.access.voice.update()
@@ -266,19 +260,10 @@ class Editor:
266
260
  self.window.core.config.set(key, value)
267
261
  self.window.controller.ui.update_font_size()
268
262
 
269
- # update markdown
270
- elif key == "theme.markdown":
271
- self.window.core.config.set(key, value)
272
- self.window.controller.theme.markdown.update(force=True)
273
-
274
263
  elif key == "render.code_syntax":
275
264
  self.window.core.config.set(key, value)
276
265
  self.window.controller.theme.toggle_syntax(value, update_menu=True)
277
266
 
278
- elif key == "ctx.convert_lists":
279
- self.window.core.config.set(key, value)
280
- self.window.controller.ctx.refresh()
281
-
282
267
  elif key == "ctx.records.separators":
283
268
  self.window.core.config.set(key, value)
284
269
  self.window.controller.ctx.update()
@@ -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: 2025.09.26 13:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -33,6 +33,8 @@ class Profile:
33
33
  self.height = 500
34
34
  self.initialized = False
35
35
  self.dialog_initialized = False
36
+ self.before_theme = None
37
+ self.before_language = None
36
38
 
37
39
  def setup(self):
38
40
  """Setup profile"""
@@ -54,7 +56,8 @@ class Profile:
54
56
  uuid: str,
55
57
  force: bool = False,
56
58
  save_current: bool = True,
57
- on_finish: Optional[callable] = None
59
+ on_finish: Optional[callable] = None,
60
+ is_create: bool = False,
58
61
  ):
59
62
  """
60
63
  Switch profile
@@ -63,6 +66,7 @@ class Profile:
63
66
  :param force: Force switch
64
67
  :param save_current: Save current profile
65
68
  :param on_finish: Callback function to call after switch
69
+ :param is_create: Is called from create profile
66
70
  """
67
71
  current = self.window.core.config.profile.get_current()
68
72
  if uuid == current and not force:
@@ -85,7 +89,8 @@ class Profile:
85
89
  self.window.controller.settings.workdir.update(
86
90
  path,
87
91
  force=True,
88
- profile_name=profile['name']
92
+ profile_name=profile['name'],
93
+ is_create=is_create,
89
94
  )
90
95
  else:
91
96
  self.after_update(profile['name'])
@@ -288,7 +293,14 @@ class Profile:
288
293
 
289
294
  :param uuid: profile UUID
290
295
  """
291
- self.switch(uuid, force=True, on_finish=self.after_create_finish)
296
+ self.before_theme = self.window.core.config.get("theme")
297
+ self.before_language = self.window.core.config.get("lang")
298
+ self.switch(
299
+ uuid,
300
+ force=True,
301
+ on_finish=self.after_create_finish,
302
+ is_create=True
303
+ )
292
304
 
293
305
  def after_create_finish(self, uuid: str):
294
306
  """
@@ -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.20 23:00:00 #
9
+ # Updated Date: 2025.09.26 13:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
@@ -288,13 +288,15 @@ class Workdir:
288
288
  def update_workdir(
289
289
  self,
290
290
  force: bool = False,
291
- path: str = None
291
+ path: str = None,
292
+ is_create: bool = False,
292
293
  ):
293
294
  """
294
295
  Update working directory
295
296
 
296
297
  :param force: boolean indicating if update should be forced (confirm)
297
298
  :param path: new working directory to set
299
+ :param is_create: True if called on profile creation
298
300
  """
299
301
  print("\n====================")
300
302
  print(f"Changing workdir to: {path}")
@@ -313,8 +315,25 @@ class Workdir:
313
315
  # update path in current profile
314
316
  self.window.core.config.profile.update_current_workdir(path)
315
317
 
318
+ # save previous theme and language to retain them after workdir change
319
+ prev_theme = None
320
+ prev_lang = None
321
+ if is_create:
322
+ prev_theme = self.window.core.config.get('theme')
323
+ prev_lang = self.window.core.config.get('lang')
324
+
316
325
  # reload config
317
326
  self.window.core.config.set_workdir(path, reload=True)
327
+
328
+ # if profile is just created, use current theme and language
329
+ if is_create:
330
+ print("Using current theme and language: ", prev_theme, prev_lang)
331
+ if prev_theme is not None:
332
+ self.window.core.config.set('theme', prev_theme)
333
+ if prev_lang is not None:
334
+ self.window.core.config.set('lang', prev_lang)
335
+ self.window.core.config.save()
336
+
318
337
  self.window.core.config.set('license.accepted', True) # accept license to prevent show dialog again
319
338
 
320
339
  @Slot(bool, str, str, str)
@@ -323,7 +342,8 @@ class Workdir:
323
342
  force: bool,
324
343
  profile_name: str,
325
344
  current_path: str,
326
- new_path: str
345
+ new_path: str,
346
+ is_create: bool = False
327
347
  ) -> bool:
328
348
  """
329
349
  Update working directory
@@ -332,18 +352,20 @@ class Workdir:
332
352
  :param profile_name: profile name to update after workdir change
333
353
  :param current_path: current working directory before update
334
354
  :param new_path: new working directory to set
355
+ :param is_create: if True, skip check for existing workdir in path
335
356
  :return: boolean indicating if update was successful
336
357
  """
337
358
  self.update_workdir(
338
359
  force=force,
339
360
  path=new_path,
361
+ is_create=is_create,
340
362
  )
341
363
  rollback = False
342
364
  success = False
343
365
  if force:
344
366
  try:
345
367
  self.window.ui.dialogs.workdir.show_status(trans("dialog.workdir.result.wait"))
346
- self.window.controller.reload()
368
+ self.window.controller.reload() # reload all
347
369
  self.window.ui.dialogs.workdir.show_status(trans("dialog.workdir.result.wait"))
348
370
  msg = trans("dialog.workdir.result.success").format(path=new_path)
349
371
  self.window.ui.dialogs.workdir.show_status(msg)
@@ -498,7 +520,8 @@ class Workdir:
498
520
  self,
499
521
  path: str,
500
522
  force: bool = False,
501
- profile_name: str = None
523
+ profile_name: str = None,
524
+ is_create: bool = False,
502
525
  ):
503
526
  """
504
527
  Switch working directory to the existing one
@@ -506,12 +529,14 @@ class Workdir:
506
529
  :param path: existing working directory
507
530
  :param force: force update (confirm)
508
531
  :param profile_name: profile name (optional, for future use)
532
+ :param is_create: if True, skip check for existing workdir in path
509
533
  """
510
534
  self.do_update(
511
535
  force=force,
512
536
  profile_name=profile_name,
513
537
  current_path=self.window.core.config.get_user_path(),
514
538
  new_path=path,
539
+ is_create=is_create,
515
540
  )
516
541
 
517
542
  def migrate(
@@ -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.22 15:00:00 #
9
+ # Updated Date: 2025.09.26 13:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -32,6 +32,8 @@ class Common:
32
32
  :return: custom css filename (e.g. style.dark.css)
33
33
  """
34
34
  # check per theme style css
35
+ if name is None:
36
+ name = ""
35
37
  filename = 'style.css'
36
38
  if filename is not None:
37
39
  # per theme mode (light / dark)
@@ -58,7 +60,7 @@ class Common:
58
60
 
59
61
  :return: True if light theme, False otherwise
60
62
  """
61
- theme = self.window.core.config.get('theme')
63
+ theme = str(self.window.core.config.get('theme'))
62
64
  return theme.startswith('light_') or theme == 'light'
63
65
 
64
66
  def toggle_tooltips(self):