dulus 0.2.0__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.
- agent.py +363 -0
- backend/__init__.py +63 -0
- backend/compressor.py +261 -0
- backend/context.py +329 -0
- backend/githook.py +166 -0
- backend/marketplace.py +141 -0
- backend/mempalace_bridge.py +182 -0
- backend/personas.py +297 -0
- backend/plugins.py +222 -0
- backend/server.py +411 -0
- backend/tasks.py +213 -0
- batch_api.py +307 -0
- checkpoint/__init__.py +27 -0
- checkpoint/hooks.py +90 -0
- checkpoint/store.py +314 -0
- checkpoint/types.py +80 -0
- claude_code_watcher.py +214 -0
- clipboard_utils.py +246 -0
- cloudsave.py +159 -0
- common.py +177 -0
- compaction.py +378 -0
- config.py +180 -0
- context.py +241 -0
- dulus-0.2.0.dist-info/METADATA +600 -0
- dulus-0.2.0.dist-info/RECORD +101 -0
- dulus-0.2.0.dist-info/WHEEL +5 -0
- dulus-0.2.0.dist-info/entry_points.txt +2 -0
- dulus-0.2.0.dist-info/licenses/LICENSE +674 -0
- dulus-0.2.0.dist-info/licenses/license_manager.py +187 -0
- dulus-0.2.0.dist-info/top_level.txt +36 -0
- dulus.py +8455 -0
- dulus_gui.py +331 -0
- dulus_mcp/__init__.py +43 -0
- dulus_mcp/client.py +546 -0
- dulus_mcp/config.py +133 -0
- dulus_mcp/tools.py +131 -0
- dulus_mcp/types.py +124 -0
- gui/__init__.py +18 -0
- gui/agent_bridge.py +283 -0
- gui/chat_widget.py +448 -0
- gui/main_window.py +485 -0
- gui/personas.py +230 -0
- gui/session_utils.py +189 -0
- gui/settings_dialog.py +146 -0
- gui/sidebar.py +515 -0
- gui/tasks_view.py +499 -0
- gui/themes.py +256 -0
- gui/tool_panel.py +94 -0
- input.py +1030 -0
- license_manager.py +187 -0
- memory/__init__.py +93 -0
- memory/audit.py +51 -0
- memory/consolidator.py +312 -0
- memory/context.py +270 -0
- memory/offload.py +148 -0
- memory/palace.py +127 -0
- memory/scan.py +146 -0
- memory/sessions.py +100 -0
- memory/store.py +395 -0
- memory/tools.py +408 -0
- memory/types.py +114 -0
- memory/vector_search.py +92 -0
- multi_agent/__init__.py +23 -0
- multi_agent/subagent.py +501 -0
- multi_agent/tools.py +393 -0
- offload_helper.py +183 -0
- plugin/__init__.py +22 -0
- plugin/autoadapter.py +1641 -0
- plugin/loader.py +156 -0
- plugin/recommend.py +211 -0
- plugin/store.py +387 -0
- plugin/types.py +147 -0
- providers.py +3750 -0
- skill/__init__.py +14 -0
- skill/builtin.py +100 -0
- skill/clawhub.py +270 -0
- skill/executor.py +66 -0
- skill/loader.py +199 -0
- skill/tools.py +110 -0
- skills.py +14 -0
- spinner.py +42 -0
- string_utils.py +42 -0
- subagent.py +11 -0
- task/__init__.py +12 -0
- task/store.py +199 -0
- task/tools.py +265 -0
- task/types.py +92 -0
- tmux_offloader.py +177 -0
- tmux_tools.py +410 -0
- tool_registry.py +214 -0
- tools.py +2694 -0
- ui/__init__.py +1 -0
- ui/input.py +464 -0
- ui/render.py +272 -0
- voice/__init__.py +56 -0
- voice/keyterms.py +179 -0
- voice/recorder.py +263 -0
- voice/stt.py +408 -0
- voice/tts.py +570 -0
- webchat.py +432 -0
- webchat_server.py +1761 -0
gui/sidebar.py
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"""Left sidebar panel for Dulus GUI.
|
|
2
|
+
|
|
3
|
+
Provides session history, model selector, context-usage bar,
|
|
4
|
+
quick-command buttons, available-tools list, and version info.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Callable
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import customtkinter as ctk
|
|
15
|
+
HAS_CTK = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
import tkinter as ctk
|
|
18
|
+
from tkinter import ttk
|
|
19
|
+
HAS_CTK = False
|
|
20
|
+
|
|
21
|
+
from config import CONFIG_DIR, SESSIONS_DIR, DAILY_DIR, MR_SESSION_DIR, load_config
|
|
22
|
+
from tool_registry import get_all_tools
|
|
23
|
+
from providers import PROVIDERS, list_ollama_models
|
|
24
|
+
from gui.themes import get_theme, CURATED_MODELS
|
|
25
|
+
from gui.session_utils import build_title, scan_sessions, delete_session
|
|
26
|
+
|
|
27
|
+
# ── Theme constants (mirror main_window.py when available) ──────────────────
|
|
28
|
+
BG_COLOR = "#1a1a2e"
|
|
29
|
+
CARD_COLOR = "#16213e"
|
|
30
|
+
ACCENT_COLOR = "#00BCD4"
|
|
31
|
+
ACCENT_HOVER = "#00acc1"
|
|
32
|
+
MAGENTA_ACCENT = "#e91e63"
|
|
33
|
+
TEXT_COLOR = "#eaeaea"
|
|
34
|
+
TEXT_DIM = "#a0a0a0"
|
|
35
|
+
BORDER_COLOR = "#2a2a4a"
|
|
36
|
+
SIDEBAR_WIDTH = 260
|
|
37
|
+
|
|
38
|
+
FONT_FAMILY = "Segoe UI"
|
|
39
|
+
FONT_NORMAL = (FONT_FAMILY, 12)
|
|
40
|
+
FONT_BOLD = (FONT_FAMILY, 12, "bold")
|
|
41
|
+
FONT_SMALL = (FONT_FAMILY, 10)
|
|
42
|
+
|
|
43
|
+
# Dulus version
|
|
44
|
+
_VERSION = "unknown"
|
|
45
|
+
try:
|
|
46
|
+
import dulus as _dulus_mod
|
|
47
|
+
_VERSION = getattr(_dulus_mod, "VERSION", _VERSION)
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DulusSidebar(ctk.CTkFrame if HAS_CTK else ctk.Frame):
|
|
53
|
+
"""Left sidebar with session history, model selector, context bar, tools, and quick commands."""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
master,
|
|
58
|
+
bridge=None,
|
|
59
|
+
on_new_chat: Callable[[], None] | None = None,
|
|
60
|
+
on_command: Callable[[str], None] | None = None,
|
|
61
|
+
on_model_change: Callable[[str], None] | None = None,
|
|
62
|
+
on_session_select: Callable[[str], None] | None = None,
|
|
63
|
+
**kwargs,
|
|
64
|
+
):
|
|
65
|
+
if HAS_CTK:
|
|
66
|
+
kwargs.setdefault("width", SIDEBAR_WIDTH)
|
|
67
|
+
kwargs.setdefault("fg_color", CARD_COLOR)
|
|
68
|
+
kwargs.setdefault("corner_radius", 0)
|
|
69
|
+
else:
|
|
70
|
+
kwargs.setdefault("width", SIDEBAR_WIDTH)
|
|
71
|
+
kwargs.setdefault("bg", CARD_COLOR)
|
|
72
|
+
|
|
73
|
+
super().__init__(master, **kwargs)
|
|
74
|
+
self.bridge = bridge
|
|
75
|
+
self.on_new_chat = on_new_chat
|
|
76
|
+
self.on_command = on_command
|
|
77
|
+
self.on_model_change = on_model_change
|
|
78
|
+
self.on_session_select = on_session_select
|
|
79
|
+
self.on_settings: Callable[[], None] | None = None
|
|
80
|
+
|
|
81
|
+
self._model_var = ctk.StringVar(value="")
|
|
82
|
+
self._sessions: list[str] = []
|
|
83
|
+
self._session_buttons: dict[str, ctk.CTkButton] = {}
|
|
84
|
+
self._quick_cmd_buttons: list = []
|
|
85
|
+
self._tool_labels: list = []
|
|
86
|
+
|
|
87
|
+
self._active_session_id: str | None = None
|
|
88
|
+
self._session_cache: dict[str, dict] = {}
|
|
89
|
+
|
|
90
|
+
self._build_ui()
|
|
91
|
+
self._refresh_model_list()
|
|
92
|
+
|
|
93
|
+
# ── UI construction ───────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
def _build_ui(self) -> None:
|
|
96
|
+
# Make the sidebar fixed-width and scrollable
|
|
97
|
+
if HAS_CTK:
|
|
98
|
+
self.grid_propagate(False)
|
|
99
|
+
self.grid_columnconfigure(0, weight=1)
|
|
100
|
+
|
|
101
|
+
# Scrollable frame container
|
|
102
|
+
if HAS_CTK:
|
|
103
|
+
container = ctk.CTkScrollableFrame(self, fg_color="transparent", width=SIDEBAR_WIDTH - 20)
|
|
104
|
+
else:
|
|
105
|
+
container = ctk.Frame(self, bg=CARD_COLOR)
|
|
106
|
+
# Simple scrollbar for tkinter fallback
|
|
107
|
+
canvas = ctk.Canvas(container, bg=CARD_COLOR, highlightthickness=0)
|
|
108
|
+
scrollbar = ttk.Scrollbar(container, orient="vertical", command=canvas.yview)
|
|
109
|
+
scroll_frame = ctk.Frame(canvas, bg=CARD_COLOR)
|
|
110
|
+
scroll_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
|
111
|
+
canvas.create_window((0, 0), window=scroll_frame, anchor="nw")
|
|
112
|
+
canvas.configure(yscrollcommand=scrollbar.set)
|
|
113
|
+
canvas.pack(side="left", fill="both", expand=True)
|
|
114
|
+
scrollbar.pack(side="right", fill="y")
|
|
115
|
+
container = scroll_frame
|
|
116
|
+
|
|
117
|
+
container.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
|
|
118
|
+
self.grid_rowconfigure(0, weight=1)
|
|
119
|
+
self._sidebar_container = container
|
|
120
|
+
|
|
121
|
+
# ── Header / Logo ────────────────────────────────────────────────────
|
|
122
|
+
lbl_cls = ctk.CTkLabel if HAS_CTK else ctk.Label
|
|
123
|
+
self._logo_label = lbl_cls(container, text="🦅 Dulus", font=(FONT_FAMILY, 18, "bold"),
|
|
124
|
+
text_color=ACCENT_COLOR if HAS_CTK else ACCENT_COLOR)
|
|
125
|
+
self._logo_label.pack(anchor="w", pady=(0, 2))
|
|
126
|
+
self._subtitle_label = lbl_cls(container, text="AI Coding Assistant", font=FONT_SMALL,
|
|
127
|
+
text_color=TEXT_DIM if HAS_CTK else TEXT_DIM)
|
|
128
|
+
self._subtitle_label.pack(anchor="w", pady=(0, 10))
|
|
129
|
+
|
|
130
|
+
# Accent separator
|
|
131
|
+
sep = ctk.CTkFrame if HAS_CTK else ctk.Frame
|
|
132
|
+
self._accent_sep = sep(container, height=2, fg_color=ACCENT_COLOR if HAS_CTK else ACCENT_COLOR,
|
|
133
|
+
**({"bg": ACCENT_COLOR} if not HAS_CTK else {}))
|
|
134
|
+
self._accent_sep.pack(fill="x", pady=(0, 12))
|
|
135
|
+
|
|
136
|
+
# ── New Chat button ──────────────────────────────────────────────────
|
|
137
|
+
btn_cls = ctk.CTkButton if HAS_CTK else ctk.Button
|
|
138
|
+
self._new_chat_btn = btn_cls(
|
|
139
|
+
container,
|
|
140
|
+
text="+ Nueva conversación",
|
|
141
|
+
font=FONT_BOLD,
|
|
142
|
+
fg_color=ACCENT_COLOR if HAS_CTK else ACCENT_COLOR,
|
|
143
|
+
hover_color=ACCENT_HOVER if HAS_CTK else ACCENT_HOVER,
|
|
144
|
+
text_color=BG_COLOR if HAS_CTK else BG_COLOR,
|
|
145
|
+
**({"bg": ACCENT_COLOR} if not HAS_CTK else {}),
|
|
146
|
+
command=self._on_new_chat_click,
|
|
147
|
+
)
|
|
148
|
+
self._new_chat_btn.pack(fill="x", pady=(0, 12))
|
|
149
|
+
|
|
150
|
+
# ── Session list ─────────────────────────────────────────────────────
|
|
151
|
+
self._sess_label = lbl_cls(container, text="Historial", font=FONT_BOLD,
|
|
152
|
+
text_color=TEXT_COLOR if HAS_CTK else TEXT_COLOR)
|
|
153
|
+
self._sess_label.pack(anchor="w", pady=(0, 6))
|
|
154
|
+
|
|
155
|
+
frame_cls = ctk.CTkFrame if HAS_CTK else ctk.Frame
|
|
156
|
+
self.session_frame = frame_cls(container, fg_color="transparent" if HAS_CTK else CARD_COLOR,
|
|
157
|
+
**({"bg": CARD_COLOR} if not HAS_CTK else {}))
|
|
158
|
+
self.session_frame.pack(fill="x", pady=(0, 12))
|
|
159
|
+
|
|
160
|
+
# ── Model selector ───────────────────────────────────────────────────
|
|
161
|
+
self._model_label = lbl_cls(container, text="Modelo activo", font=FONT_BOLD,
|
|
162
|
+
text_color=TEXT_COLOR if HAS_CTK else TEXT_COLOR)
|
|
163
|
+
self._model_label.pack(anchor="w", pady=(0, 6))
|
|
164
|
+
|
|
165
|
+
if HAS_CTK:
|
|
166
|
+
self.model_combo = ctk.CTkOptionMenu(
|
|
167
|
+
container,
|
|
168
|
+
values=[],
|
|
169
|
+
variable=self._model_var,
|
|
170
|
+
command=self._on_model_change,
|
|
171
|
+
font=FONT_NORMAL,
|
|
172
|
+
dropdown_font=FONT_NORMAL,
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
self.model_combo = ttk.Combobox(container, textvariable=self._model_var, state="readonly")
|
|
176
|
+
self.model_combo.bind("<<ComboboxSelected>>", lambda e: self._on_model_change(self._model_var.get()))
|
|
177
|
+
|
|
178
|
+
self.model_combo.pack(fill="x", pady=(0, 12))
|
|
179
|
+
|
|
180
|
+
# ── Context usage bar ────────────────────────────────────────────────
|
|
181
|
+
self._ctx_label = lbl_cls(container, text="Uso de contexto", font=FONT_BOLD,
|
|
182
|
+
text_color=TEXT_COLOR if HAS_CTK else TEXT_COLOR)
|
|
183
|
+
self._ctx_label.pack(anchor="w", pady=(0, 6))
|
|
184
|
+
|
|
185
|
+
self.context_label = lbl_cls(
|
|
186
|
+
container, text="0 / 250000", font=FONT_SMALL,
|
|
187
|
+
text_color=TEXT_DIM if HAS_CTK else TEXT_DIM,
|
|
188
|
+
)
|
|
189
|
+
self.context_label.pack(anchor="w")
|
|
190
|
+
|
|
191
|
+
if HAS_CTK:
|
|
192
|
+
self.context_bar = ctk.CTkProgressBar(
|
|
193
|
+
container, height=8, corner_radius=4,
|
|
194
|
+
progress_color=ACCENT_COLOR,
|
|
195
|
+
fg_color=BORDER_COLOR,
|
|
196
|
+
)
|
|
197
|
+
self.context_bar.set(0.0)
|
|
198
|
+
else:
|
|
199
|
+
self.context_bar = ttk.Progressbar(container, length=SIDEBAR_WIDTH - 40, mode="determinate")
|
|
200
|
+
self.context_bar["value"] = 0
|
|
201
|
+
|
|
202
|
+
self.context_bar.pack(fill="x", pady=(4, 12))
|
|
203
|
+
|
|
204
|
+
# ── Quick commands ───────────────────────────────────────────────────
|
|
205
|
+
self._cmd_label = lbl_cls(container, text="Comandos rápidos", font=FONT_BOLD,
|
|
206
|
+
text_color=TEXT_COLOR if HAS_CTK else TEXT_COLOR)
|
|
207
|
+
self._cmd_label.pack(anchor="w", pady=(0, 6))
|
|
208
|
+
|
|
209
|
+
self._cmd_frame = frame_cls(container, fg_color="transparent" if HAS_CTK else CARD_COLOR,
|
|
210
|
+
**({"bg": CARD_COLOR} if not HAS_CTK else {}))
|
|
211
|
+
self._cmd_frame.pack(fill="x", pady=(0, 12))
|
|
212
|
+
|
|
213
|
+
for col, (cmd, label) in enumerate([
|
|
214
|
+
("/clear", "/clear"),
|
|
215
|
+
("/context", "/context"),
|
|
216
|
+
("/cost", "/cost"),
|
|
217
|
+
("/verbose", "/verbose"),
|
|
218
|
+
]):
|
|
219
|
+
btn = btn_cls(
|
|
220
|
+
self._cmd_frame,
|
|
221
|
+
text=label,
|
|
222
|
+
font=FONT_SMALL,
|
|
223
|
+
width=60,
|
|
224
|
+
height=28,
|
|
225
|
+
fg_color=BORDER_COLOR if HAS_CTK else BORDER_COLOR,
|
|
226
|
+
hover_color=ACCENT_HOVER if HAS_CTK else ACCENT_HOVER,
|
|
227
|
+
text_color=TEXT_COLOR if HAS_CTK else TEXT_COLOR,
|
|
228
|
+
**({"bg": BORDER_COLOR} if not HAS_CTK else {}),
|
|
229
|
+
command=lambda c=cmd: self._on_command_click(c),
|
|
230
|
+
)
|
|
231
|
+
btn.grid(row=0, column=col, padx=2, pady=2)
|
|
232
|
+
self._quick_cmd_buttons.append(btn)
|
|
233
|
+
|
|
234
|
+
# ── Bottom buttons (outside scroll area) ─────────────────────────────
|
|
235
|
+
self._bottom_frame = frame_cls(self, fg_color="transparent" if HAS_CTK else CARD_COLOR,
|
|
236
|
+
**({"bg": CARD_COLOR} if not HAS_CTK else {}))
|
|
237
|
+
self._bottom_frame.grid(row=1, column=0, sticky="ew", padx=10, pady=(0, 10))
|
|
238
|
+
self._bottom_frame.grid_columnconfigure(0, weight=1)
|
|
239
|
+
|
|
240
|
+
self._settings_btn = btn_cls(
|
|
241
|
+
self._bottom_frame,
|
|
242
|
+
text="⚙ Ajustes",
|
|
243
|
+
font=FONT_NORMAL,
|
|
244
|
+
fg_color="transparent" if HAS_CTK else CARD_COLOR,
|
|
245
|
+
hover_color=BORDER_COLOR if HAS_CTK else BORDER_COLOR,
|
|
246
|
+
text_color=TEXT_COLOR if HAS_CTK else TEXT_COLOR,
|
|
247
|
+
**({"bg": CARD_COLOR} if not HAS_CTK else {}),
|
|
248
|
+
corner_radius=10,
|
|
249
|
+
height=36,
|
|
250
|
+
border_width=1 if HAS_CTK else 0,
|
|
251
|
+
border_color=BORDER_COLOR if HAS_CTK else BORDER_COLOR,
|
|
252
|
+
command=self._on_settings_click,
|
|
253
|
+
)
|
|
254
|
+
self._settings_btn.grid(row=0, column=0, sticky="ew")
|
|
255
|
+
|
|
256
|
+
# ── Refresh helpers ───────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
def set_sessions(self, sessions: list[dict]) -> None:
|
|
259
|
+
"""Update the session history list in the sidebar."""
|
|
260
|
+
# Clear existing buttons
|
|
261
|
+
for widget in getattr(self.session_frame, "winfo_children", lambda: [])():
|
|
262
|
+
widget.destroy()
|
|
263
|
+
self._session_buttons.clear()
|
|
264
|
+
self._sessions = []
|
|
265
|
+
|
|
266
|
+
if not sessions:
|
|
267
|
+
lbl = ctk.CTkLabel if HAS_CTK else ctk.Label
|
|
268
|
+
lbl(self.session_frame, text="(sin sesiones)", font=FONT_SMALL,
|
|
269
|
+
text_color=TEXT_DIM if HAS_CTK else TEXT_DIM).pack(anchor="w")
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
btn_cls = ctk.CTkButton if HAS_CTK else ctk.Button
|
|
273
|
+
frm_cls = ctk.CTkFrame if HAS_CTK else ctk.Frame
|
|
274
|
+
|
|
275
|
+
for sess in sessions:
|
|
276
|
+
sid = sess.get("id", "")
|
|
277
|
+
title = sess.get("title", "Untitled")
|
|
278
|
+
self._sessions.append(sid)
|
|
279
|
+
|
|
280
|
+
# Row container for button + delete
|
|
281
|
+
row = frm_cls(self.session_frame, fg_color="transparent" if HAS_CTK else CARD_COLOR)
|
|
282
|
+
row.pack(fill="x", pady=1)
|
|
283
|
+
row.grid_columnconfigure(0, weight=1)
|
|
284
|
+
|
|
285
|
+
# Main session button
|
|
286
|
+
btn = btn_cls(
|
|
287
|
+
row,
|
|
288
|
+
text=title,
|
|
289
|
+
font=FONT_SMALL,
|
|
290
|
+
fg_color="transparent" if HAS_CTK else CARD_COLOR,
|
|
291
|
+
hover_color=BORDER_COLOR if HAS_CTK else BORDER_COLOR,
|
|
292
|
+
text_color=TEXT_DIM if HAS_CTK else TEXT_DIM,
|
|
293
|
+
**({"bg": CARD_COLOR} if not HAS_CTK else {}),
|
|
294
|
+
anchor="w",
|
|
295
|
+
height=28,
|
|
296
|
+
command=lambda s=sid: self._on_session_click(s),
|
|
297
|
+
)
|
|
298
|
+
btn.grid(row=0, column=0, sticky="ew")
|
|
299
|
+
self._session_buttons[sid] = btn
|
|
300
|
+
|
|
301
|
+
# Delete button (X)
|
|
302
|
+
del_btn = btn_cls(
|
|
303
|
+
row,
|
|
304
|
+
text=" \u2715 ", # Unicode X
|
|
305
|
+
width=24,
|
|
306
|
+
height=24,
|
|
307
|
+
font=(FONT_FAMILY, 10, "bold"),
|
|
308
|
+
fg_color="transparent" if HAS_CTK else CARD_COLOR,
|
|
309
|
+
hover_color="#aa3333", # Reddish on hover
|
|
310
|
+
text_color=TEXT_DIM if HAS_CTK else TEXT_DIM,
|
|
311
|
+
command=lambda s=sid: self._on_delete_click(s),
|
|
312
|
+
)
|
|
313
|
+
del_btn.grid(row=0, column=1, padx=(0, 2))
|
|
314
|
+
|
|
315
|
+
self._highlight_active_session()
|
|
316
|
+
|
|
317
|
+
def _on_delete_click(self, session_id: str) -> None:
|
|
318
|
+
"""Handle session deletion from the sidebar."""
|
|
319
|
+
if delete_session(session_id):
|
|
320
|
+
# Refresh the list
|
|
321
|
+
self.refresh_sessions()
|
|
322
|
+
# If the deleted session was active, reset chat
|
|
323
|
+
if self._active_session_id == session_id:
|
|
324
|
+
if self.on_new_chat:
|
|
325
|
+
self.on_new_chat()
|
|
326
|
+
|
|
327
|
+
def set_active_session(self, session_id: str | None) -> None:
|
|
328
|
+
"""Mark a session as active in the sidebar."""
|
|
329
|
+
self._active_session_id = session_id
|
|
330
|
+
self._highlight_active_session()
|
|
331
|
+
|
|
332
|
+
def _highlight_active_session(self) -> None:
|
|
333
|
+
if not HAS_CTK:
|
|
334
|
+
return
|
|
335
|
+
for sid, btn in self._session_buttons.items():
|
|
336
|
+
if sid == self._active_session_id:
|
|
337
|
+
btn.configure(
|
|
338
|
+
fg_color=ACCENT_COLOR,
|
|
339
|
+
text_color=BG_COLOR,
|
|
340
|
+
)
|
|
341
|
+
else:
|
|
342
|
+
btn.configure(
|
|
343
|
+
fg_color="transparent",
|
|
344
|
+
text_color=TEXT_DIM,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def refresh_sessions(self) -> None:
|
|
348
|
+
"""Load session history from all session directories (auto-scan)."""
|
|
349
|
+
data = scan_sessions()
|
|
350
|
+
self._session_cache = {s["id"]: s for s in data}
|
|
351
|
+
self.set_sessions(data)
|
|
352
|
+
def _on_settings_click(self) -> None:
|
|
353
|
+
if self.on_settings:
|
|
354
|
+
self.on_settings()
|
|
355
|
+
|
|
356
|
+
def _on_session_click(self, sid: str) -> None:
|
|
357
|
+
if self.on_session_select:
|
|
358
|
+
self.on_session_select(sid)
|
|
359
|
+
|
|
360
|
+
def _refresh_tools(self) -> None:
|
|
361
|
+
"""Populate the tools list from the registry."""
|
|
362
|
+
for widget in getattr(self.tools_frame, "winfo_children", lambda: [])():
|
|
363
|
+
widget.destroy()
|
|
364
|
+
|
|
365
|
+
tools = get_all_tools()
|
|
366
|
+
if not tools:
|
|
367
|
+
lbl = ctk.CTkLabel if HAS_CTK else ctk.Label
|
|
368
|
+
lbl(self.tools_frame, text="(ninguna tool cargada)", font=FONT_SMALL,
|
|
369
|
+
text_color=TEXT_DIM if HAS_CTK else TEXT_DIM).pack(anchor="w")
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
lbl_cls = ctk.CTkLabel if HAS_CTK else ctk.Label
|
|
373
|
+
for t in tools:
|
|
374
|
+
name = t.name
|
|
375
|
+
lbl_cls(
|
|
376
|
+
self.tools_frame,
|
|
377
|
+
text=f"• {name}",
|
|
378
|
+
font=FONT_SMALL,
|
|
379
|
+
text_color=TEXT_DIM if HAS_CTK else TEXT_DIM,
|
|
380
|
+
).pack(anchor="w")
|
|
381
|
+
|
|
382
|
+
def _refresh_model_list(self) -> None:
|
|
383
|
+
"""Populate the model dropdown from curated list."""
|
|
384
|
+
models = CURATED_MODELS
|
|
385
|
+
|
|
386
|
+
current = self.bridge.config.get("model", models[0]) if self.bridge else models[0]
|
|
387
|
+
self._model_var.set(current)
|
|
388
|
+
|
|
389
|
+
if HAS_CTK:
|
|
390
|
+
self.model_combo.configure(values=models)
|
|
391
|
+
else:
|
|
392
|
+
self.model_combo["values"] = models
|
|
393
|
+
|
|
394
|
+
def update_context_bar(self) -> None:
|
|
395
|
+
"""Refresh the context usage progress bar (call from UI thread)."""
|
|
396
|
+
if not self.bridge:
|
|
397
|
+
return
|
|
398
|
+
used, limit = self.bridge.get_context_usage()
|
|
399
|
+
pct = min(used / limit, 1.0) if limit else 0.0
|
|
400
|
+
|
|
401
|
+
self.context_label.configure(
|
|
402
|
+
text=f"{used:,} / {limit:,}"
|
|
403
|
+
) if HAS_CTK else self.context_label.config(text=f"{used:,} / {limit:,}")
|
|
404
|
+
|
|
405
|
+
if HAS_CTK:
|
|
406
|
+
self.context_bar.set(pct)
|
|
407
|
+
# Color coding: green -> yellow -> red
|
|
408
|
+
if pct < 0.5:
|
|
409
|
+
self.context_bar.configure(progress_color=ACCENT_COLOR)
|
|
410
|
+
elif pct < 0.8:
|
|
411
|
+
self.context_bar.configure(progress_color="#FFC107")
|
|
412
|
+
else:
|
|
413
|
+
self.context_bar.configure(progress_color="#F44336")
|
|
414
|
+
else:
|
|
415
|
+
self.context_bar["value"] = pct * 100
|
|
416
|
+
|
|
417
|
+
# ── Event handlers ────────────────────────────────────────────────────────
|
|
418
|
+
|
|
419
|
+
def _on_new_chat_click(self) -> None:
|
|
420
|
+
if self.bridge:
|
|
421
|
+
self.bridge.clear_session()
|
|
422
|
+
if self.on_new_chat:
|
|
423
|
+
self.on_new_chat()
|
|
424
|
+
self.update_context_bar()
|
|
425
|
+
|
|
426
|
+
def _on_command_click(self, cmd: str) -> None:
|
|
427
|
+
if self.on_command:
|
|
428
|
+
self.on_command(cmd)
|
|
429
|
+
# Local handling for commands that affect bridge state directly
|
|
430
|
+
if cmd == "/clear" and self.bridge:
|
|
431
|
+
self.bridge.clear_session()
|
|
432
|
+
self.update_context_bar()
|
|
433
|
+
elif cmd == "/verbose" and self.bridge:
|
|
434
|
+
current = self.bridge.config.get("verbose", False)
|
|
435
|
+
self.bridge.config["verbose"] = not current
|
|
436
|
+
|
|
437
|
+
def _on_model_change(self, model: str) -> None:
|
|
438
|
+
if self.bridge:
|
|
439
|
+
self.bridge.set_model(model)
|
|
440
|
+
if self.on_model_change:
|
|
441
|
+
self.on_model_change(model)
|
|
442
|
+
|
|
443
|
+
def _on_session_click(self, path: str) -> None:
|
|
444
|
+
if self.on_session_select:
|
|
445
|
+
self.on_session_select(path)
|
|
446
|
+
|
|
447
|
+
def apply_theme(self, t: dict | None = None) -> None:
|
|
448
|
+
"""Re-apply current theme colors to all sidebar widgets."""
|
|
449
|
+
if t is None:
|
|
450
|
+
t = get_theme()
|
|
451
|
+
if not HAS_CTK:
|
|
452
|
+
return
|
|
453
|
+
# Update module-level globals so future widgets pick them up
|
|
454
|
+
global BG_COLOR, CARD_COLOR, ACCENT_COLOR, ACCENT_HOVER, TEXT_COLOR, TEXT_DIM, BORDER_COLOR
|
|
455
|
+
BG_COLOR = t["bg"]
|
|
456
|
+
CARD_COLOR = t["card"]
|
|
457
|
+
ACCENT_COLOR = t["accent"]
|
|
458
|
+
ACCENT_HOVER = t["accent_hover"]
|
|
459
|
+
TEXT_COLOR = t["text"]
|
|
460
|
+
TEXT_DIM = t["dim"]
|
|
461
|
+
BORDER_COLOR = t["border"]
|
|
462
|
+
# Main frame
|
|
463
|
+
self.configure(fg_color=t["card"])
|
|
464
|
+
if hasattr(self, "_sidebar_container"):
|
|
465
|
+
self._sidebar_container.configure(fg_color="transparent")
|
|
466
|
+
|
|
467
|
+
# Internal frames
|
|
468
|
+
if hasattr(self, "session_frame"):
|
|
469
|
+
self.session_frame.configure(fg_color="transparent")
|
|
470
|
+
if hasattr(self, "_cmd_frame"):
|
|
471
|
+
self._cmd_frame.configure(fg_color="transparent")
|
|
472
|
+
if hasattr(self, "_bottom_frame"):
|
|
473
|
+
self._bottom_frame.configure(fg_color="transparent")
|
|
474
|
+
|
|
475
|
+
self.update_idletasks()
|
|
476
|
+
# Logo
|
|
477
|
+
self._logo_label.configure(text_color=t["accent"])
|
|
478
|
+
self._subtitle_label.configure(text_color=t["dim"])
|
|
479
|
+
self._accent_sep.configure(fg_color=t["accent"])
|
|
480
|
+
# New chat button
|
|
481
|
+
self._new_chat_btn.configure(
|
|
482
|
+
fg_color=t["accent"], hover_color=t["accent_hover"], text_color=t["bg"]
|
|
483
|
+
)
|
|
484
|
+
# Labels
|
|
485
|
+
self._sess_label.configure(text_color=t["text"])
|
|
486
|
+
self._model_label.configure(text_color=t["text"])
|
|
487
|
+
self._ctx_label.configure(text_color=t["text"])
|
|
488
|
+
self._cmd_label.configure(text_color=t["text"])
|
|
489
|
+
# Separators
|
|
490
|
+
# Model combo
|
|
491
|
+
self.model_combo.configure(
|
|
492
|
+
fg_color=t["card"], button_color=t["border"],
|
|
493
|
+
button_hover_color=t["accent_hover"], text_color=t["text"],
|
|
494
|
+
dropdown_text_color=t["text"], dropdown_fg_color=t["card"],
|
|
495
|
+
)
|
|
496
|
+
# Context bar
|
|
497
|
+
self.context_label.configure(text_color=t["dim"])
|
|
498
|
+
self.context_bar.configure(fg_color=t["border"], progress_color=t["accent"])
|
|
499
|
+
# Quick cmd buttons
|
|
500
|
+
for btn in self._quick_cmd_buttons:
|
|
501
|
+
btn.configure(
|
|
502
|
+
fg_color=t["border"], hover_color=t["accent_hover"], text_color=t["text"]
|
|
503
|
+
)
|
|
504
|
+
# Session buttons
|
|
505
|
+
for btn in self._session_buttons.values():
|
|
506
|
+
btn.configure(
|
|
507
|
+
fg_color="transparent", hover_color=t["border"], text_color=t["text"]
|
|
508
|
+
)
|
|
509
|
+
# Tool labels
|
|
510
|
+
for lbl in self._tool_labels:
|
|
511
|
+
lbl.configure(text_color=t["dim"])
|
|
512
|
+
# Settings button
|
|
513
|
+
self._settings_btn.configure(
|
|
514
|
+
hover_color=t["border"], text_color=t["text"], border_color=t["border"]
|
|
515
|
+
)
|