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.
- pygpt_net/CHANGELOG.txt +14 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/controller/chat/common.py +115 -6
- pygpt_net/controller/chat/input.py +4 -1
- pygpt_net/controller/chat/response.py +8 -2
- pygpt_net/controller/presets/presets.py +121 -6
- pygpt_net/controller/settings/editor.py +0 -15
- pygpt_net/controller/settings/profile.py +16 -4
- pygpt_net/controller/settings/workdir.py +30 -5
- pygpt_net/controller/theme/common.py +4 -2
- pygpt_net/controller/theme/markdown.py +4 -7
- pygpt_net/controller/theme/theme.py +2 -1
- pygpt_net/controller/ui/ui.py +32 -7
- pygpt_net/core/agents/custom/__init__.py +7 -1
- pygpt_net/core/agents/custom/llama_index/factory.py +17 -6
- pygpt_net/core/agents/custom/llama_index/runner.py +52 -4
- pygpt_net/core/agents/custom/llama_index/utils.py +12 -1
- pygpt_net/core/agents/custom/router.py +45 -6
- pygpt_net/core/agents/custom/runner.py +11 -5
- pygpt_net/core/agents/custom/schema.py +3 -1
- pygpt_net/core/agents/custom/utils.py +13 -1
- pygpt_net/core/agents/runners/llama_workflow.py +65 -5
- pygpt_net/core/agents/runners/openai_workflow.py +2 -1
- pygpt_net/core/db/viewer.py +11 -5
- pygpt_net/core/node_editor/graph.py +18 -9
- pygpt_net/core/node_editor/models.py +9 -2
- pygpt_net/core/node_editor/types.py +15 -1
- pygpt_net/core/presets/presets.py +216 -29
- pygpt_net/core/render/markdown/parser.py +0 -2
- pygpt_net/core/render/web/renderer.py +76 -11
- pygpt_net/data/config/config.json +5 -6
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +2 -38
- pygpt_net/data/css/style.dark.css +18 -0
- pygpt_net/data/css/style.light.css +20 -1
- pygpt_net/data/locale/locale.de.ini +66 -1
- pygpt_net/data/locale/locale.en.ini +64 -3
- pygpt_net/data/locale/locale.es.ini +66 -1
- pygpt_net/data/locale/locale.fr.ini +66 -1
- pygpt_net/data/locale/locale.it.ini +66 -1
- pygpt_net/data/locale/locale.pl.ini +67 -2
- pygpt_net/data/locale/locale.uk.ini +66 -1
- pygpt_net/data/locale/locale.zh.ini +66 -1
- pygpt_net/data/locale/plugin.cmd_system.en.ini +62 -66
- pygpt_net/item/ctx.py +23 -1
- pygpt_net/provider/agents/llama_index/flow_from_schema.py +2 -2
- pygpt_net/provider/agents/llama_index/workflow/codeact.py +9 -6
- pygpt_net/provider/agents/llama_index/workflow/openai.py +38 -11
- pygpt_net/provider/agents/llama_index/workflow/planner.py +36 -16
- pygpt_net/provider/agents/llama_index/workflow/supervisor.py +60 -10
- pygpt_net/provider/agents/openai/agent.py +3 -1
- pygpt_net/provider/agents/openai/agent_b2b.py +13 -9
- pygpt_net/provider/agents/openai/agent_planner.py +6 -2
- pygpt_net/provider/agents/openai/agent_with_experts.py +4 -1
- pygpt_net/provider/agents/openai/agent_with_experts_feedback.py +4 -2
- pygpt_net/provider/agents/openai/agent_with_feedback.py +4 -2
- pygpt_net/provider/agents/openai/evolve.py +6 -2
- pygpt_net/provider/agents/openai/supervisor.py +3 -1
- pygpt_net/provider/api/openai/agents/response.py +1 -0
- pygpt_net/provider/core/config/patch.py +18 -1
- pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +0 -6
- pygpt_net/tools/agent_builder/tool.py +48 -26
- pygpt_net/tools/agent_builder/ui/dialogs.py +36 -28
- pygpt_net/ui/__init__.py +2 -4
- pygpt_net/ui/dialog/about.py +58 -38
- pygpt_net/ui/dialog/db.py +142 -3
- pygpt_net/ui/dialog/preset.py +47 -8
- pygpt_net/ui/layout/toolbox/presets.py +64 -16
- pygpt_net/ui/main.py +2 -2
- pygpt_net/ui/widget/dialog/confirm.py +27 -3
- pygpt_net/ui/widget/dialog/db.py +0 -0
- pygpt_net/ui/widget/draw/painter.py +90 -1
- pygpt_net/ui/widget/lists/preset.py +908 -60
- pygpt_net/ui/widget/node_editor/command.py +10 -10
- pygpt_net/ui/widget/node_editor/config.py +157 -0
- pygpt_net/ui/widget/node_editor/editor.py +223 -153
- pygpt_net/ui/widget/node_editor/item.py +12 -11
- pygpt_net/ui/widget/node_editor/node.py +246 -13
- pygpt_net/ui/widget/node_editor/view.py +179 -63
- pygpt_net/ui/widget/tabs/output.py +1 -1
- pygpt_net/ui/widget/textarea/input.py +157 -23
- pygpt_net/utils.py +114 -2
- {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/METADATA +26 -100
- {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/RECORD +86 -85
- {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/WHEEL +0 -0
- {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.
|
|
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.
|
|
17
|
-
__build__ = "2025-09-
|
|
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.
|
|
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
|
-
|
|
467
|
-
if ctx.
|
|
468
|
-
|
|
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}{
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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):
|