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.
Files changed (101) hide show
  1. agent.py +363 -0
  2. backend/__init__.py +63 -0
  3. backend/compressor.py +261 -0
  4. backend/context.py +329 -0
  5. backend/githook.py +166 -0
  6. backend/marketplace.py +141 -0
  7. backend/mempalace_bridge.py +182 -0
  8. backend/personas.py +297 -0
  9. backend/plugins.py +222 -0
  10. backend/server.py +411 -0
  11. backend/tasks.py +213 -0
  12. batch_api.py +307 -0
  13. checkpoint/__init__.py +27 -0
  14. checkpoint/hooks.py +90 -0
  15. checkpoint/store.py +314 -0
  16. checkpoint/types.py +80 -0
  17. claude_code_watcher.py +214 -0
  18. clipboard_utils.py +246 -0
  19. cloudsave.py +159 -0
  20. common.py +177 -0
  21. compaction.py +378 -0
  22. config.py +180 -0
  23. context.py +241 -0
  24. dulus-0.2.0.dist-info/METADATA +600 -0
  25. dulus-0.2.0.dist-info/RECORD +101 -0
  26. dulus-0.2.0.dist-info/WHEEL +5 -0
  27. dulus-0.2.0.dist-info/entry_points.txt +2 -0
  28. dulus-0.2.0.dist-info/licenses/LICENSE +674 -0
  29. dulus-0.2.0.dist-info/licenses/license_manager.py +187 -0
  30. dulus-0.2.0.dist-info/top_level.txt +36 -0
  31. dulus.py +8455 -0
  32. dulus_gui.py +331 -0
  33. dulus_mcp/__init__.py +43 -0
  34. dulus_mcp/client.py +546 -0
  35. dulus_mcp/config.py +133 -0
  36. dulus_mcp/tools.py +131 -0
  37. dulus_mcp/types.py +124 -0
  38. gui/__init__.py +18 -0
  39. gui/agent_bridge.py +283 -0
  40. gui/chat_widget.py +448 -0
  41. gui/main_window.py +485 -0
  42. gui/personas.py +230 -0
  43. gui/session_utils.py +189 -0
  44. gui/settings_dialog.py +146 -0
  45. gui/sidebar.py +515 -0
  46. gui/tasks_view.py +499 -0
  47. gui/themes.py +256 -0
  48. gui/tool_panel.py +94 -0
  49. input.py +1030 -0
  50. license_manager.py +187 -0
  51. memory/__init__.py +93 -0
  52. memory/audit.py +51 -0
  53. memory/consolidator.py +312 -0
  54. memory/context.py +270 -0
  55. memory/offload.py +148 -0
  56. memory/palace.py +127 -0
  57. memory/scan.py +146 -0
  58. memory/sessions.py +100 -0
  59. memory/store.py +395 -0
  60. memory/tools.py +408 -0
  61. memory/types.py +114 -0
  62. memory/vector_search.py +92 -0
  63. multi_agent/__init__.py +23 -0
  64. multi_agent/subagent.py +501 -0
  65. multi_agent/tools.py +393 -0
  66. offload_helper.py +183 -0
  67. plugin/__init__.py +22 -0
  68. plugin/autoadapter.py +1641 -0
  69. plugin/loader.py +156 -0
  70. plugin/recommend.py +211 -0
  71. plugin/store.py +387 -0
  72. plugin/types.py +147 -0
  73. providers.py +3750 -0
  74. skill/__init__.py +14 -0
  75. skill/builtin.py +100 -0
  76. skill/clawhub.py +270 -0
  77. skill/executor.py +66 -0
  78. skill/loader.py +199 -0
  79. skill/tools.py +110 -0
  80. skills.py +14 -0
  81. spinner.py +42 -0
  82. string_utils.py +42 -0
  83. subagent.py +11 -0
  84. task/__init__.py +12 -0
  85. task/store.py +199 -0
  86. task/tools.py +265 -0
  87. task/types.py +92 -0
  88. tmux_offloader.py +177 -0
  89. tmux_tools.py +410 -0
  90. tool_registry.py +214 -0
  91. tools.py +2694 -0
  92. ui/__init__.py +1 -0
  93. ui/input.py +464 -0
  94. ui/render.py +272 -0
  95. voice/__init__.py +56 -0
  96. voice/keyterms.py +179 -0
  97. voice/recorder.py +263 -0
  98. voice/stt.py +408 -0
  99. voice/tts.py +570 -0
  100. webchat.py +432 -0
  101. 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
+ )