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/main_window.py ADDED
@@ -0,0 +1,485 @@
1
+ """Dulus Main Window β€” customtkinter desktop GUI.
2
+
3
+ Provides a professional dark-themed interface with sidebar, chat area,
4
+ input bar, and top controls. Designed to be wired to a backend bridge
5
+ by another agent.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import tkinter as tk
10
+ from typing import Callable
11
+
12
+ try:
13
+ import customtkinter as ctk
14
+ except ImportError:
15
+ raise ImportError("customtkinter is required. Install: pip install customtkinter")
16
+
17
+ from gui.chat_widget import ChatWidget
18
+ from gui.tasks_view import TasksView
19
+ from gui.themes import get_theme, set_theme, list_themes, CURATED_MODELS
20
+ from gui.sidebar import DulusSidebar
21
+
22
+ # ── Theme constants (loaded from active theme) ──────────────────────────────
23
+ _SIDEBAR_WIDTH = 260
24
+ _INPUT_HEIGHT = 60
25
+ _TOPBAR_HEIGHT = 50
26
+
27
+ _FONT_FAMILY = "Segoe UI"
28
+ _FONT_NORMAL = (_FONT_FAMILY, 13)
29
+ _FONT_BOLD = (_FONT_FAMILY, 13, "bold")
30
+ _FONT_SMALL = (_FONT_FAMILY, 11)
31
+ _FONT_TITLE = (_FONT_FAMILY, 18, "bold")
32
+ _FONT_LOGO = (_FONT_FAMILY, 22, "bold")
33
+
34
+ # Initial theme values (overridden by apply_theme)
35
+ t = get_theme()
36
+ BG_COLOR = t["bg"]
37
+ CARD_COLOR = t["card"]
38
+ ACCENT_COLOR = t["accent"]
39
+ ACCENT_HOVER = t["accent_hover"]
40
+ TEXT_COLOR = t["text"]
41
+ TEXT_DIM = t["dim"]
42
+ BORDER_COLOR = t["border"]
43
+ SIDEBAR_WIDTH = _SIDEBAR_WIDTH
44
+ INPUT_HEIGHT = _INPUT_HEIGHT
45
+ TOPBAR_HEIGHT = _TOPBAR_HEIGHT
46
+ FONT_FAMILY = _FONT_FAMILY
47
+ FONT_NORMAL = _FONT_NORMAL
48
+ FONT_BOLD = _FONT_BOLD
49
+ FONT_SMALL = _FONT_SMALL
50
+ FONT_TITLE = _FONT_TITLE
51
+ FONT_LOGO = _FONT_LOGO
52
+
53
+
54
+ class DulusMainWindow(ctk.CTk):
55
+ """Main Dulus application window."""
56
+
57
+ def __init__(self):
58
+ super().__init__()
59
+
60
+ # ── Window setup ─────────────────────────────────────────────────────
61
+ self.title("Dulus")
62
+ self.geometry("1100x750")
63
+ self.minsize(900, 600)
64
+ self.configure(fg_color=BG_COLOR)
65
+
66
+ # Theme
67
+ ctk.set_appearance_mode("dark")
68
+ ctk.set_default_color_theme("dark-blue")
69
+ self._theme_name = "midnight" # Placeholder, will be sync'd by apply_theme
70
+
71
+ # Grid layout: sidebar | main area
72
+ self.grid_columnconfigure(1, weight=1)
73
+ self.grid_rowconfigure(0, weight=1)
74
+
75
+ # ── Callback placeholders (inject from bridge) ───────────────────────
76
+ self.on_send: Callable[[str], None] = lambda text: None
77
+ self.on_new_chat: Callable[[], None] = lambda: None
78
+ self.on_settings: Callable[[], None] = lambda: None
79
+ self.on_model_change: Callable[[str], None] = lambda model: None
80
+ self.on_voice_toggle: Callable[[], None] = lambda: None
81
+ self.on_attach: Callable[[], None] = lambda: None
82
+ self.on_session_select: Callable[[str], None] = lambda sid: None
83
+
84
+ # ── Build UI ─────────────────────────────────────────────────────────
85
+ self._build_sidebar()
86
+ self._build_main_area()
87
+ # Initialize with current global theme instead of a hardcoded string
88
+ from gui.themes import get_theme, THEMES
89
+ active = get_theme()
90
+ current_theme_name = "midnight"
91
+ for name, colors in THEMES.items():
92
+ if colors["bg"] == active["bg"] and colors["accent"] == active["accent"]:
93
+ current_theme_name = name
94
+ break
95
+ self.apply_theme(current_theme_name)
96
+
97
+ # ═══════════════════════════════════════════════════════════════════════
98
+ # Sidebar
99
+ # ═══════════════════════════════════════════════════════════════════════
100
+
101
+ def _build_sidebar(self) -> None:
102
+ self.sidebar = DulusSidebar(
103
+ self,
104
+ on_new_chat=lambda: self.on_new_chat(),
105
+ on_command=lambda cmd: None,
106
+ on_model_change=lambda model: self.on_model_change(model),
107
+ on_session_select=lambda sid: self._on_sidebar_session_select(sid),
108
+ )
109
+ self.sidebar.grid(row=0, column=0, sticky="nsew")
110
+ self.sidebar.grid_propagate(False)
111
+ self._active_session_id: str | None = None
112
+
113
+ # ═══════════════════════════════════════════════════════════════════════
114
+ # Main area
115
+ # ═══════════════════════════════════════════════════════════════════════
116
+
117
+ def _build_main_area(self) -> None:
118
+ self.main_frame = ctk.CTkFrame(self, fg_color=BG_COLOR, corner_radius=0)
119
+ self.main_frame.grid(row=0, column=1, sticky="nsew")
120
+ self.main_frame.grid_columnconfigure(0, weight=1)
121
+ self.main_frame.grid_rowconfigure(1, weight=1)
122
+ self.main_frame.grid_rowconfigure(2, weight=0)
123
+
124
+ # ── Top bar ──────────────────────────────────────────────────────────
125
+ self.topbar = ctk.CTkFrame(
126
+ self.main_frame,
127
+ height=TOPBAR_HEIGHT,
128
+ fg_color=CARD_COLOR,
129
+ corner_radius=0,
130
+ )
131
+ self.topbar.grid(row=0, column=0, sticky="ew", padx=0, pady=0)
132
+ self.topbar.grid_propagate(False)
133
+ self.topbar.grid_columnconfigure(1, weight=1)
134
+
135
+ # Model selector
136
+ self.model_label = ctk.CTkLabel(
137
+ self.topbar,
138
+ text="Modelo:",
139
+ font=FONT_SMALL,
140
+ text_color=TEXT_DIM,
141
+ )
142
+ self.model_label.grid(row=0, column=0, padx=(16, 4), pady=10)
143
+
144
+ self.model_selector = ctk.CTkOptionMenu(
145
+ self.topbar,
146
+ values=CURATED_MODELS,
147
+ font=FONT_NORMAL,
148
+ dropdown_font=FONT_NORMAL,
149
+ fg_color=BG_COLOR,
150
+ button_color=BORDER_COLOR,
151
+ button_hover_color=ACCENT_HOVER,
152
+ text_color=TEXT_COLOR,
153
+ dropdown_text_color=TEXT_COLOR,
154
+ dropdown_fg_color=CARD_COLOR,
155
+ corner_radius=8,
156
+ width=180,
157
+ command=self._on_model_change,
158
+ )
159
+ self.model_selector.grid(row=0, column=1, sticky="w", pady=10)
160
+ self.model_selector.set(CURATED_MODELS[0])
161
+
162
+ # Tasks toggle button
163
+ self.tasks_btn = ctk.CTkButton(
164
+ self.topbar,
165
+ text="πŸ—‚οΈ Tareas",
166
+ font=FONT_BOLD,
167
+ fg_color="transparent",
168
+ hover_color=BORDER_COLOR,
169
+ text_color=TEXT_DIM,
170
+ corner_radius=10,
171
+ height=32,
172
+ border_width=1,
173
+ border_color=BORDER_COLOR,
174
+ command=self._toggle_tasks_view,
175
+ )
176
+ self.tasks_btn.grid(row=0, column=2, sticky="e", padx=(0, 8), pady=10)
177
+
178
+ # Status indicators
179
+ self.status_frame = ctk.CTkFrame(self.topbar, fg_color="transparent")
180
+ self.status_frame.grid(row=0, column=3, sticky="e", padx=16, pady=10)
181
+
182
+ self.status_dot = ctk.CTkLabel(
183
+ self.status_frame,
184
+ text="●",
185
+ font=(FONT_FAMILY, 14),
186
+ text_color="#4caf50",
187
+ )
188
+ self.status_dot.pack(side="left", padx=(0, 4))
189
+
190
+ self.status_label = ctk.CTkLabel(
191
+ self.status_frame,
192
+ text="Listo",
193
+ font=FONT_SMALL,
194
+ text_color=TEXT_DIM,
195
+ )
196
+ self.status_label.pack(side="left")
197
+
198
+ # ── Chat widget ──────────────────────────────────────────────────────
199
+ self.chat = ChatWidget(self.main_frame)
200
+ self.chat.grid(row=1, column=0, sticky="nsew", padx=8, pady=8)
201
+
202
+ # ── Tasks view (hidden by default) ───────────────────────────────────
203
+ self.tasks_view = TasksView(self.main_frame)
204
+ self.tasks_view.grid(row=1, column=0, sticky="nsew", padx=8, pady=8)
205
+ self.tasks_view.grid_remove()
206
+ self._tasks_visible = False
207
+
208
+ # ── Input bar ────────────────────────────────────────────────────────
209
+ self.input_frame = ctk.CTkFrame(
210
+ self.main_frame,
211
+ height=INPUT_HEIGHT,
212
+ fg_color=CARD_COLOR,
213
+ corner_radius=14,
214
+ border_width=1,
215
+ border_color=BORDER_COLOR,
216
+ )
217
+ self.input_frame.grid(row=2, column=0, sticky="ew", padx=12, pady=(0, 12))
218
+ self.input_frame.grid_propagate(False)
219
+ self.input_frame.grid_columnconfigure(1, weight=1)
220
+
221
+ # Attachment button
222
+ self.attach_btn = ctk.CTkButton(
223
+ self.input_frame,
224
+ text="πŸ“Ž",
225
+ font=(FONT_FAMILY, 16),
226
+ width=36,
227
+ height=36,
228
+ fg_color="transparent",
229
+ hover_color=BORDER_COLOR,
230
+ text_color=TEXT_DIM,
231
+ corner_radius=10,
232
+ command=self._on_attach_click,
233
+ )
234
+ self.attach_btn.grid(row=0, column=0, padx=(10, 4), pady=10)
235
+
236
+ # Text input
237
+ self.input_box = ctk.CTkTextbox(
238
+ self.input_frame,
239
+ fg_color="transparent",
240
+ text_color=TEXT_COLOR,
241
+ font=FONT_NORMAL,
242
+ wrap="word",
243
+ activate_scrollbars=False,
244
+ corner_radius=10,
245
+ height=40,
246
+ )
247
+ self.input_box.grid(row=0, column=1, sticky="ew", padx=4, pady=10)
248
+ self.input_box.bind("<KeyRelease-Return>", self._on_enter_key)
249
+ self.input_box.bind("<Shift-Return>", self._on_shift_enter)
250
+ self.input_box.bind("<KeyRelease-KP_Enter>", self._on_enter_key)
251
+
252
+ # Voice button
253
+ self.voice_btn = ctk.CTkButton(
254
+ self.input_frame,
255
+ text="πŸŽ™",
256
+ font=(FONT_FAMILY, 16),
257
+ width=36,
258
+ height=36,
259
+ fg_color="transparent",
260
+ hover_color=BORDER_COLOR,
261
+ text_color=TEXT_DIM,
262
+ corner_radius=10,
263
+ command=self._on_voice_click,
264
+ )
265
+ self.voice_btn.grid(row=0, column=2, padx=4, pady=10)
266
+
267
+ # Send button
268
+ self.send_btn = ctk.CTkButton(
269
+ self.input_frame,
270
+ text="➀",
271
+ font=(FONT_FAMILY, 18),
272
+ width=40,
273
+ height=40,
274
+ fg_color=ACCENT_COLOR,
275
+ hover_color=ACCENT_HOVER,
276
+ text_color=BG_COLOR,
277
+ corner_radius=12,
278
+ command=self._on_send_click,
279
+ )
280
+ self.send_btn.grid(row=0, column=3, padx=(4, 10), pady=10)
281
+
282
+ # ═══════════════════════════════════════════════════════════════════════
283
+ # Event handlers
284
+ # ═══════════════════════════════════════════════════════════════════════
285
+
286
+ def _on_send_click(self) -> None:
287
+ text = self.input_box.get("1.0", "end-1c").strip()
288
+ if text:
289
+ self.chat.add_user_message(text)
290
+ self.input_box.delete("1.0", "end")
291
+ self.on_send(text)
292
+
293
+ def _on_enter_key(self, event=None) -> str:
294
+ # Only send if Shift is NOT held
295
+ if event and event.state & 0x1:
296
+ return "" # Shift held β€” let default newline happen
297
+ self._on_send_click()
298
+ return "break"
299
+
300
+ def _on_shift_enter(self, event=None) -> str:
301
+ self.input_box.insert("insert", "\n")
302
+ return "break"
303
+
304
+ def _on_new_chat_click(self) -> None:
305
+ self.chat.clear_chat()
306
+ self.on_new_chat()
307
+
308
+ def _on_settings_click(self) -> None:
309
+ self.on_settings()
310
+
311
+ def _on_model_change(self, model: str) -> None:
312
+ self.on_model_change(model)
313
+
314
+ def _on_voice_click(self) -> None:
315
+ self.on_voice_toggle()
316
+
317
+ def _on_attach_click(self) -> None:
318
+ self.on_attach()
319
+
320
+ def _toggle_tasks_view(self) -> None:
321
+ if self._tasks_visible:
322
+ self._show_chat_view()
323
+ else:
324
+ self._show_tasks_view()
325
+
326
+ def _show_tasks_view(self) -> None:
327
+ self.chat.grid_remove()
328
+ self.input_frame.grid_remove()
329
+ self.tasks_view.grid()
330
+ self.tasks_view.refresh()
331
+ self.tasks_btn.configure(
332
+ text="πŸ’¬ Chat",
333
+ fg_color=ACCENT_COLOR,
334
+ hover_color=ACCENT_HOVER,
335
+ text_color=BG_COLOR,
336
+ border_width=0,
337
+ )
338
+ self._tasks_visible = True
339
+
340
+ def _show_chat_view(self) -> None:
341
+ self.tasks_view.grid_remove()
342
+ self.chat.grid()
343
+ self.input_frame.grid()
344
+ self.tasks_btn.configure(
345
+ text="πŸ—‚οΈ Tareas",
346
+ fg_color="transparent",
347
+ hover_color=BORDER_COLOR,
348
+ text_color=TEXT_DIM,
349
+ border_width=1,
350
+ border_color=BORDER_COLOR,
351
+ )
352
+ self._tasks_visible = False
353
+
354
+ # ═══════════════════════════════════════════════════════════════════════
355
+ # Public API for bridge / external controllers
356
+ # ═══════════════════════════════════════════════════════════════════════
357
+
358
+ def set_status(self, text: str, color: str = TEXT_DIM) -> None:
359
+ """Update the status label and dot color."""
360
+ self.status_label.configure(text=text, text_color=color)
361
+ self.status_dot.configure(text_color=color)
362
+
363
+ def set_model(self, model: str) -> None:
364
+ """Set the model selector value."""
365
+ self.model_selector.set(model)
366
+
367
+ def _on_sidebar_session_select(self, sid: str) -> None:
368
+ self.set_active_session(sid)
369
+ self.on_session_select(sid)
370
+
371
+ def set_sessions(self, sessions: list[dict]) -> None:
372
+ """Populate the sidebar session list."""
373
+ self.sidebar.set_sessions(sessions)
374
+
375
+ def set_active_session(self, session_id: str | None) -> None:
376
+ """Mark a session as active in the sidebar."""
377
+ self._active_session_id = session_id
378
+ self.sidebar.set_active_session(session_id)
379
+
380
+ def show_thinking(self) -> None:
381
+ """Show assistant thinking indicator."""
382
+ self.chat.show_thinking()
383
+ self.set_status("Pensando…", ACCENT_COLOR)
384
+
385
+ def hide_thinking(self) -> None:
386
+ """Hide thinking indicator."""
387
+ self.chat.hide_thinking()
388
+ self.set_status("Listo", "#4caf50")
389
+
390
+ def add_assistant_chunk(self, text: str) -> None:
391
+ """Append streaming text to the current assistant message."""
392
+ self.chat.append_to_last_message(text)
393
+
394
+ def add_tool_call(self, name: str, status: str = "running") -> None:
395
+ """Show a tool execution pill."""
396
+ self.chat.add_tool_indicator(name, status)
397
+
398
+ def focus_input(self) -> None:
399
+ """Move focus to the input box."""
400
+ self.input_box.focus_set()
401
+
402
+ def apply_theme(self, theme_name: str) -> None:
403
+ """Apply a color theme to the main window widgets."""
404
+ t = set_theme(theme_name)
405
+ if not t:
406
+ return
407
+ self._theme_name = theme_name
408
+ global BG_COLOR, CARD_COLOR, ACCENT_COLOR, ACCENT_HOVER, TEXT_COLOR, TEXT_DIM, BORDER_COLOR
409
+ BG_COLOR = t["bg"]
410
+ CARD_COLOR = t["card"]
411
+ ACCENT_COLOR = t["accent"]
412
+ ACCENT_HOVER = t["accent_hover"]
413
+ TEXT_COLOR = t["text"]
414
+ TEXT_DIM = t["dim"]
415
+ BORDER_COLOR = t["border"]
416
+
417
+ # 1. Update main window backgrounds first (atomic visual shift)
418
+ self.configure(fg_color=BG_COLOR)
419
+ self.main_frame.configure(fg_color=BG_COLOR)
420
+ self.update_idletasks() # Force redraw of main area before children
421
+
422
+ # 2. Update top-level containers
423
+ self.topbar.configure(fg_color=CARD_COLOR)
424
+ self.input_frame.configure(fg_color=CARD_COLOR, border_color=BORDER_COLOR)
425
+
426
+ # 3. Update widgets
427
+ self.model_selector.configure(
428
+ fg_color=BG_COLOR, button_color=BORDER_COLOR,
429
+ button_hover_color=ACCENT_HOVER, text_color=TEXT_COLOR,
430
+ dropdown_text_color=TEXT_COLOR, dropdown_fg_color=CARD_COLOR,
431
+ )
432
+ self.send_btn.configure(fg_color=ACCENT_COLOR, hover_color=ACCENT_HOVER, text_color=BG_COLOR)
433
+ self.attach_btn.configure(hover_color=BORDER_COLOR, text_color=TEXT_DIM)
434
+ self.voice_btn.configure(hover_color=BORDER_COLOR, text_color=TEXT_DIM)
435
+ self.input_box.configure(text_color=TEXT_COLOR)
436
+ self.tasks_btn.configure(
437
+ hover_color=BORDER_COLOR, text_color=TEXT_DIM, border_color=BORDER_COLOR
438
+ )
439
+ self.status_label.configure(text_color=TEXT_DIM)
440
+ self.status_dot.configure(text_color=t.get("success", "#4caf50"))
441
+ self.model_label.configure(text_color=TEXT_DIM)
442
+
443
+ # Redraw all frames to ensure consistency
444
+ self.update()
445
+ if self._tasks_visible:
446
+ self.tasks_btn.configure(
447
+ fg_color=ACCENT_COLOR, hover_color=ACCENT_HOVER, text_color=BG_COLOR, border_width=0
448
+ )
449
+ else:
450
+ self.tasks_btn.configure(
451
+ hover_color=BORDER_COLOR, text_color=TEXT_DIM, border_color=BORDER_COLOR, border_width=1
452
+ )
453
+
454
+ # 4. Propagate to children
455
+ self.sidebar.apply_theme()
456
+
457
+ import gui.chat_widget as _cw
458
+ _cw.BG_COLOR = BG_COLOR
459
+ _cw.CARD_COLOR = CARD_COLOR
460
+ _cw.ACCENT_COLOR = ACCENT_COLOR
461
+ _cw.ACCENT_HOVER = ACCENT_HOVER
462
+ _cw.TEXT_COLOR = TEXT_COLOR
463
+ _cw.TEXT_DIM = TEXT_DIM
464
+ _cw.USER_BUBBLE = t["user_bubble"]
465
+ _cw.ASSISTANT_BUBBLE = t["assistant_bubble"]
466
+ _cw.CODE_BG = t["code_bg"]
467
+ _cw.BORDER_COLOR = BORDER_COLOR
468
+ self.chat.apply_theme()
469
+
470
+ import gui.tasks_view as _tv
471
+ _tv.BG_COLOR = BG_COLOR
472
+ _tv.CARD_COLOR = CARD_COLOR
473
+ _tv.ACCENT_COLOR = ACCENT_COLOR
474
+ _tv.ACCENT_HOVER = ACCENT_HOVER
475
+ _tv.TEXT_COLOR = TEXT_COLOR
476
+ _tv.TEXT_DIM = TEXT_DIM
477
+ _tv.BORDER_COLOR = BORDER_COLOR
478
+ if hasattr(self.tasks_view, "apply_theme"):
479
+ self.tasks_view.apply_theme()
480
+
481
+ self.update_idletasks()
482
+
483
+ def run(self) -> None:
484
+ """Start the main loop."""
485
+ self.mainloop()