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/tasks_view.py ADDED
@@ -0,0 +1,499 @@
1
+ """Dulus Tasks View — professional Kanban-style task board v2.
2
+
3
+ Reads tasks from .dulus-context/tasks.json and displays them in a
4
+ three-column layout: Pending | In Progress | Completed.
5
+
6
+ v2 improvements:
7
+ - Filter by owner (agent) and phase (week)
8
+ - Priority badges (CRITICAL/HIGH/MEDIUM/LOW)
9
+ - Agent color coding
10
+ - Auto-refresh via file polling
11
+ - Phase grouping separators
12
+ - Owner summary stats
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import datetime
18
+ import os
19
+ import threading
20
+ from pathlib import Path
21
+ from typing import Dict, List, Callable
22
+
23
+ try:
24
+ import customtkinter as ctk
25
+ HAS_CTK = True
26
+ except ImportError:
27
+ import tkinter as ctk
28
+ HAS_CTK = False
29
+
30
+ from gui.themes import get_theme
31
+
32
+ # ── Theme constants ───────────────────────────────────────────────────────────
33
+ BG_COLOR = "#1a1a2e"
34
+ CARD_COLOR = "#16213e"
35
+ ACCENT_COLOR = "#00BCD4"
36
+ ACCENT_HOVER = "#00acc1"
37
+ MAGENTA_ACCENT = "#e91e63"
38
+ TEXT_COLOR = "#eaeaea"
39
+ TEXT_DIM = "#a0a0a0"
40
+ BORDER_COLOR = "#2a2a4a"
41
+ SUCCESS_COLOR = "#4caf50"
42
+ WARNING_COLOR = "#FFC107"
43
+ ERROR_COLOR = "#F44336"
44
+ PENDING_COLOR = "#FF9800"
45
+
46
+ # ── Agent colors ──────────────────────────────────────────────────────────────
47
+ AGENT_COLORS: Dict[str, str] = {
48
+ "kimi-code": "#00BCD4",
49
+ "kimi-code2": "#e91e63",
50
+ "kimi-code3": "#4caf50",
51
+ "": "#9E9E9E",
52
+ }
53
+
54
+ # ── Priority colors ───────────────────────────────────────────────────────────
55
+ PRIORITY_COLORS: Dict[str, str] = {
56
+ "CRITICAL": "#F44336",
57
+ "HIGH": "#FF5722",
58
+ "MEDIUM": "#FF9800",
59
+ "LOW": "#9E9E9E",
60
+ }
61
+
62
+ FONT_FAMILY = "Segoe UI"
63
+ FONT_NORMAL = (FONT_FAMILY, 12)
64
+ FONT_BOLD = (FONT_FAMILY, 12, "bold")
65
+ FONT_SMALL = (FONT_FAMILY, 10)
66
+ FONT_TITLE = (FONT_FAMILY, 16, "bold")
67
+
68
+ TASKS_PATH = Path(__file__).parent.parent / ".dulus-context" / "tasks.json"
69
+ POLL_MS = 5000 # 5 seconds
70
+
71
+
72
+ def _fmt_date(iso: str) -> str:
73
+ try:
74
+ dt = datetime.datetime.fromisoformat(iso)
75
+ return dt.strftime("%d/%m %H:%M")
76
+ except Exception:
77
+ return iso
78
+
79
+
80
+ class TaskCard(ctk.CTkFrame if HAS_CTK else ctk.Frame):
81
+ """A single task card widget with priority, agent color, and phase."""
82
+
83
+ def __init__(self, master, task: dict, **kwargs):
84
+ fg = kwargs.pop("fg_color", CARD_COLOR)
85
+ super().__init__(master, fg_color=fg, corner_radius=12, border_width=1,
86
+ border_color=BORDER_COLOR, **kwargs)
87
+ self.task = task
88
+ self._expanded = False
89
+ self._build()
90
+
91
+ def _build(self) -> None:
92
+ t = self.task
93
+ status = t.get("status", "pending")
94
+ subject = t.get("subject", "Sin titulo")
95
+ description = t.get("description", "")
96
+ owner = t.get("owner", "")
97
+ blocked_by = t.get("blocked_by", [])
98
+ task_id = t.get("id", "?")
99
+ updated = _fmt_date(t.get("updated_at", ""))
100
+ metadata = t.get("metadata", {})
101
+ phase = metadata.get("phase", "")
102
+ priority = metadata.get("priority", "")
103
+
104
+ agent_color = AGENT_COLORS.get(owner, AGENT_COLORS[""])
105
+
106
+ # ── Top accent bar (agent color) ─────────────────────────────────────
107
+ accent_bar = ctk.CTkFrame(self, fg_color=agent_color, height=3, corner_radius=0)
108
+ accent_bar.pack(fill="x", padx=0, pady=0)
109
+ accent_bar.pack_propagate(False)
110
+
111
+ # ── Header row ───────────────────────────────────────────────────────
112
+ header = ctk.CTkFrame(self, fg_color="transparent")
113
+ header.pack(fill="x", padx=12, pady=(10, 4))
114
+
115
+ id_lbl = ctk.CTkLabel(
116
+ header, text=f"#{task_id}", font=FONT_SMALL, text_color=TEXT_DIM,
117
+ )
118
+ id_lbl.pack(side="left")
119
+
120
+ # Priority badge
121
+ if priority and priority in PRIORITY_COLORS:
122
+ pri_frame = ctk.CTkFrame(
123
+ header, fg_color=PRIORITY_COLORS[priority] + "30",
124
+ corner_radius=4, height=18,
125
+ )
126
+ pri_frame.pack(side="right", padx=(4, 0))
127
+ pri_frame.pack_propagate(False)
128
+ ctk.CTkLabel(
129
+ pri_frame, text=priority[:3], font=(FONT_FAMILY, 8, "bold"),
130
+ text_color=PRIORITY_COLORS[priority], width=32,
131
+ ).pack(padx=2)
132
+
133
+ # Phase mini-badge
134
+ if phase:
135
+ short_phase = phase.replace("Semana ", "W").replace(":", "")
136
+ ph_frame = ctk.CTkFrame(
137
+ header, fg_color=BORDER_COLOR, corner_radius=4, height=18,
138
+ )
139
+ ph_frame.pack(side="right", padx=(4, 0))
140
+ ph_frame.pack_propagate(False)
141
+ ctk.CTkLabel(
142
+ ph_frame, text=short_phase, font=(FONT_FAMILY, 8),
143
+ text_color=TEXT_DIM, width=60,
144
+ ).pack(padx=2)
145
+
146
+ # ── Title ────────────────────────────────────────────────────────────
147
+ title_lbl = ctk.CTkLabel(
148
+ self, text=subject, font=FONT_BOLD, text_color=TEXT_COLOR,
149
+ wraplength=280, justify="left",
150
+ )
151
+ title_lbl.pack(anchor="w", padx=12, pady=(2, 4))
152
+
153
+ # ── Short description ────────────────────────────────────────────────
154
+ short_desc = (description[:120] + "...") if len(description) > 120 else description
155
+ self.desc_lbl = ctk.CTkLabel(
156
+ self, text=short_desc, font=FONT_SMALL, text_color=TEXT_DIM,
157
+ wraplength=280, justify="left",
158
+ )
159
+ self.desc_lbl.pack(anchor="w", padx=12, pady=(0, 6))
160
+
161
+ # ── Expand button ────────────────────────────────────────────────────
162
+ if len(description) > 120:
163
+ self.expand_btn = ctk.CTkButton(
164
+ self, text="Ver mas", font=FONT_SMALL, fg_color="transparent",
165
+ hover_color=BORDER_COLOR, text_color=ACCENT_COLOR, height=24, width=80,
166
+ command=self._toggle_expand,
167
+ )
168
+ self.expand_btn.pack(anchor="w", padx=12, pady=(0, 4))
169
+ self.full_desc = description
170
+
171
+ # ── Metadata row ─────────────────────────────────────────────────────
172
+ meta = ctk.CTkFrame(self, fg_color="transparent")
173
+ meta.pack(fill="x", padx=12, pady=(4, 10))
174
+
175
+ if owner:
176
+ ctk.CTkLabel(
177
+ meta, text=f"@{owner}", font=FONT_SMALL,
178
+ text_color=agent_color,
179
+ ).pack(side="left")
180
+
181
+ ctk.CTkLabel(
182
+ meta, text=f"{updated}", font=FONT_SMALL, text_color=TEXT_DIM,
183
+ ).pack(side="right")
184
+
185
+ # ── Blocked by badge ─────────────────────────────────────────────────
186
+ if blocked_by:
187
+ block_frame = ctk.CTkFrame(self, fg_color="#3e1a24", corner_radius=6)
188
+ block_frame.pack(fill="x", padx=12, pady=(0, 10))
189
+ ctk.CTkLabel(
190
+ block_frame,
191
+ text=f"Bloqueada por: {', '.join(f'#{b}' for b in blocked_by)}",
192
+ font=FONT_SMALL, text_color=ERROR_COLOR,
193
+ ).pack(padx=8, pady=4)
194
+
195
+ def _toggle_expand(self) -> None:
196
+ if self._expanded:
197
+ short = (self.full_desc[:120] + "...") if len(self.full_desc) > 120 else self.full_desc
198
+ self.desc_lbl.configure(text=short)
199
+ self.expand_btn.configure(text="Ver mas")
200
+ self._expanded = False
201
+ else:
202
+ self.desc_lbl.configure(text=self.full_desc)
203
+ self.expand_btn.configure(text="Ver menos")
204
+ self._expanded = True
205
+
206
+
207
+ class TasksView(ctk.CTkFrame if HAS_CTK else ctk.Frame):
208
+ """Professional Kanban task board for Dulus with filters and auto-refresh."""
209
+
210
+ def __init__(self, master, tasks_file: Path | str | None = None, **kwargs):
211
+ super().__init__(master, fg_color=BG_COLOR, corner_radius=0, **kwargs)
212
+ self.tasks_file = Path(tasks_file) if tasks_file else TASKS_PATH
213
+ self._columns: Dict[str, ctk.CTkScrollableFrame] = {}
214
+ self._count_labels: Dict[str, ctk.CTkLabel] = {}
215
+ self._column_headers: Dict[str, ctk.CTkFrame] = {}
216
+ self._column_containers: Dict[str, ctk.CTkFrame] = {}
217
+ self._column_title_labels: Dict[str, ctk.CTkLabel] = {}
218
+ self._owner_filter: str = ""
219
+ self._phase_filter: str = ""
220
+ self._last_mtime: float = 0.0
221
+ self._poll_after_id: str | None = None
222
+ self._build_ui()
223
+ self.refresh()
224
+ self._start_polling()
225
+
226
+ def _build_ui(self) -> None:
227
+ self.grid_columnconfigure(0, weight=1)
228
+ self.grid_rowconfigure(2, weight=1)
229
+
230
+ # ── Top toolbar ──────────────────────────────────────────────────────
231
+ toolbar = ctk.CTkFrame(self, fg_color="transparent", height=50)
232
+ toolbar.grid(row=0, column=0, sticky="ew", padx=16, pady=(16, 8))
233
+ toolbar.grid_propagate(False)
234
+
235
+ title = ctk.CTkLabel(
236
+ toolbar, text="Dulus Task Board", font=(FONT_FAMILY, 20, "bold"),
237
+ text_color=ACCENT_COLOR,
238
+ )
239
+ title.pack(side="left")
240
+
241
+ # Owner filter
242
+ self.owner_var = ctk.StringVar(value="Todos")
243
+ owner_opts = ["Todos", "kimi-code", "kimi-code2", "kimi-code3", "Sin owner"]
244
+ self._owner_menu = ctk.CTkOptionMenu(
245
+ toolbar, values=owner_opts, variable=self.owner_var,
246
+ font=FONT_SMALL, dropdown_font=FONT_SMALL,
247
+ fg_color=CARD_COLOR, button_color=BORDER_COLOR,
248
+ button_hover_color=ACCENT_HOVER, text_color=TEXT_COLOR,
249
+ width=120, height=30, command=lambda _: self.refresh(),
250
+ )
251
+ self._owner_menu.pack(side="right", padx=(8, 0))
252
+ ctk.CTkLabel(toolbar, text="Agente:", font=FONT_SMALL, text_color=TEXT_DIM).pack(side="right")
253
+
254
+ # Phase filter
255
+ self.phase_var = ctk.StringVar(value="Todas")
256
+ phase_opts = [
257
+ "Todas", "Semana 1: Fundamentos", "Semana 2: Entry Points",
258
+ "Semana 3: Plataforma", "Semana 4: Ecosistema", "Legacy",
259
+ ]
260
+ self._phase_menu = ctk.CTkOptionMenu(
261
+ toolbar, values=phase_opts, variable=self.phase_var,
262
+ font=FONT_SMALL, dropdown_font=FONT_SMALL,
263
+ fg_color=CARD_COLOR, button_color=BORDER_COLOR,
264
+ button_hover_color=ACCENT_HOVER, text_color=TEXT_COLOR,
265
+ width=160, height=30, command=lambda _: self.refresh(),
266
+ )
267
+ self._phase_menu.pack(side="right", padx=(8, 0))
268
+ ctk.CTkLabel(toolbar, text="Fase:", font=FONT_SMALL, text_color=TEXT_DIM).pack(side="right")
269
+
270
+ # Refresh button
271
+ self.refresh_btn = ctk.CTkButton(
272
+ toolbar, text="Refrescar", font=FONT_BOLD,
273
+ fg_color=ACCENT_COLOR, hover_color=ACCENT_HOVER,
274
+ text_color=BG_COLOR, corner_radius=10, height=34,
275
+ command=self.refresh,
276
+ )
277
+ self.refresh_btn.pack(side="right", padx=(16, 0))
278
+
279
+ # ── Agent summary bar ────────────────────────────────────────────────
280
+ summary = ctk.CTkFrame(self, fg_color="transparent", height=30)
281
+ summary.grid(row=1, column=0, sticky="ew", padx=16, pady=(0, 8))
282
+ summary.grid_propagate(False)
283
+
284
+ self._agent_labels: Dict[str, ctk.CTkLabel] = {}
285
+ for agent, color in AGENT_COLORS.items():
286
+ if not agent:
287
+ continue
288
+ lbl = ctk.CTkLabel(
289
+ summary, text=f"@{agent}: 0", font=FONT_SMALL,
290
+ text_color=color,
291
+ )
292
+ lbl.pack(side="left", padx=(0, 16))
293
+ self._agent_labels[agent] = lbl
294
+
295
+ self._total_label = ctk.CTkLabel(
296
+ summary, text="Total: 0", font=FONT_SMALL, text_color=TEXT_DIM,
297
+ )
298
+ self._total_label.pack(side="right")
299
+
300
+ # ── Columns container ────────────────────────────────────────────────
301
+ cols_frame = ctk.CTkFrame(self, fg_color="transparent")
302
+ cols_frame.grid(row=2, column=0, sticky="nsew", padx=8, pady=8)
303
+ cols_frame.grid_columnconfigure(0, weight=1)
304
+ cols_frame.grid_columnconfigure(1, weight=1)
305
+ cols_frame.grid_columnconfigure(2, weight=1)
306
+ cols_frame.grid_rowconfigure(0, weight=1)
307
+
308
+ self._columns_container = cols_frame
309
+ self._create_column(cols_frame, 0, "Pendiente", PENDING_COLOR, "pending")
310
+ self._create_column(cols_frame, 1, "En Progreso", ACCENT_COLOR, "in_progress")
311
+ self._create_column(cols_frame, 2, "Completadas", SUCCESS_COLOR, "completed")
312
+
313
+ def _create_column(self, parent, col: int, title: str, color: str, status_key: str) -> None:
314
+ container = ctk.CTkFrame(parent, fg_color=BG_COLOR, corner_radius=0)
315
+ container.grid(row=0, column=col, sticky="nsew", padx=8, pady=0)
316
+ container.grid_columnconfigure(0, weight=1)
317
+ container.grid_rowconfigure(1, weight=1)
318
+ self._column_containers[status_key] = container
319
+
320
+ hdr = ctk.CTkFrame(container, fg_color=CARD_COLOR, corner_radius=10, height=40)
321
+ hdr.grid(row=0, column=0, sticky="ew", pady=(0, 8))
322
+ hdr.grid_propagate(False)
323
+ self._column_headers[status_key] = hdr
324
+
325
+ title_lbl = ctk.CTkLabel(
326
+ hdr, text=title, font=FONT_BOLD, text_color=color,
327
+ )
328
+ title_lbl.pack(side="left", padx=12, pady=4)
329
+ self._column_title_labels[status_key] = title_lbl
330
+
331
+ count_lbl = ctk.CTkLabel(
332
+ hdr, text="0", font=FONT_BOLD, text_color=TEXT_DIM,
333
+ )
334
+ count_lbl.pack(side="right", padx=12, pady=4)
335
+ self._count_labels[status_key] = count_lbl
336
+
337
+ scroll = ctk.CTkScrollableFrame(
338
+ container, fg_color="transparent", corner_radius=0,
339
+ scrollbar_fg_color=BORDER_COLOR,
340
+ scrollbar_button_color=ACCENT_COLOR,
341
+ scrollbar_button_hover_color=ACCENT_HOVER,
342
+ )
343
+ scroll.grid(row=1, column=0, sticky="nsew")
344
+ self._columns[status_key] = scroll
345
+
346
+ def _load_tasks(self) -> List[dict]:
347
+ try:
348
+ data = json.loads(self.tasks_file.read_text(encoding="utf-8"))
349
+ return data.get("tasks", [])
350
+ except Exception:
351
+ return []
352
+
353
+ def _matches_filters(self, task: dict) -> bool:
354
+ owner = task.get("owner", "")
355
+ metadata = task.get("metadata", {})
356
+ phase = metadata.get("phase", "")
357
+
358
+ owner_filter = self.owner_var.get()
359
+ if owner_filter == "Sin owner":
360
+ if owner:
361
+ return False
362
+ elif owner_filter != "Todos" and owner != owner_filter:
363
+ return False
364
+
365
+ phase_filter = self.phase_var.get()
366
+ if phase_filter != "Todas":
367
+ if phase_filter == "Legacy":
368
+ if phase:
369
+ return False
370
+ elif phase != phase_filter:
371
+ return False
372
+
373
+ return True
374
+
375
+ def refresh(self) -> None:
376
+ # Clear columns
377
+ for scroll in self._columns.values():
378
+ for widget in scroll.winfo_children():
379
+ widget.destroy()
380
+
381
+ tasks = self._load_tasks()
382
+ counts: Dict[str, int] = {"pending": 0, "in_progress": 0, "completed": 0, "cancelled": 0}
383
+ agent_counts: Dict[str, int] = {"kimi-code": 0, "kimi-code2": 0, "kimi-code3": 0}
384
+
385
+ # Filter and sort
386
+ filtered = [t for t in tasks if self._matches_filters(t)]
387
+ status_order = {"in_progress": 0, "pending": 1, "completed": 2, "cancelled": 3}
388
+ filtered.sort(key=lambda t: status_order.get(t.get("status", ""), 99))
389
+
390
+ for task in filtered:
391
+ status = task.get("status", "pending")
392
+ counts[status] = counts.get(status, 0) + 1
393
+
394
+ owner = task.get("owner", "")
395
+ if owner in agent_counts:
396
+ agent_counts[owner] += 1
397
+
398
+ col_key = status if status in self._columns else "pending"
399
+ scroll = self._columns.get(col_key)
400
+ if scroll is None:
401
+ continue
402
+
403
+ card = TaskCard(scroll, task)
404
+ card.pack(fill="x", pady=(0, 10), padx=2)
405
+
406
+ # Update column counters
407
+ for key, lbl in self._count_labels.items():
408
+ lbl.configure(text=str(counts.get(key, 0)))
409
+
410
+ # Update agent summary
411
+ for agent, lbl in self._agent_labels.items():
412
+ lbl.configure(text=f"@{agent}: {agent_counts.get(agent, 0)}")
413
+
414
+ total = len(filtered)
415
+ done = counts.get("completed", 0)
416
+ pct = int((done / total) * 100) if total else 0
417
+ self._total_label.configure(text=f"Total: {total} | {pct}% done")
418
+ self.refresh_btn.configure(text="Refrescar")
419
+
420
+ # Update last mtime
421
+ try:
422
+ self._last_mtime = self.tasks_file.stat().st_mtime
423
+ except Exception:
424
+ pass
425
+
426
+ def _check_file_changed(self) -> None:
427
+ try:
428
+ mtime = self.tasks_file.stat().st_mtime
429
+ if mtime != self._last_mtime:
430
+ self.refresh()
431
+ except Exception:
432
+ pass
433
+ self._poll_after_id = self.after(POLL_MS, self._check_file_changed)
434
+
435
+ def _start_polling(self) -> None:
436
+ self._check_file_changed()
437
+
438
+ def apply_theme(self) -> None:
439
+ """Re-apply current theme colors to persistent widgets."""
440
+ t = get_theme()
441
+ global BG_COLOR, CARD_COLOR, ACCENT_COLOR, ACCENT_HOVER, TEXT_COLOR, TEXT_DIM, BORDER_COLOR
442
+ BG_COLOR = t["bg"]
443
+ CARD_COLOR = t["card"]
444
+ ACCENT_COLOR = t["accent"]
445
+ ACCENT_HOVER = t["accent_hover"]
446
+ TEXT_COLOR = t["text"]
447
+ TEXT_DIM = t["dim"]
448
+ BORDER_COLOR = t["border"]
449
+
450
+ self.configure(fg_color=t["bg"])
451
+ self.refresh_btn.configure(
452
+ fg_color=t["accent"], hover_color=t["accent_hover"], text_color=t["bg"]
453
+ )
454
+ self._owner_menu.configure(
455
+ fg_color=t["card"], button_color=t["border"],
456
+ button_hover_color=t["accent_hover"], text_color=t["text"],
457
+ )
458
+ self._phase_menu.configure(
459
+ fg_color=t["card"], button_color=t["border"],
460
+ button_hover_color=t["accent_hover"], text_color=t["text"],
461
+ )
462
+ self._total_label.configure(text_color=t["dim"])
463
+ for lbl in self._agent_labels.values():
464
+ # agent colors are fixed; only dim text updates
465
+ pass
466
+ for lbl in self._count_labels.values():
467
+ lbl.configure(text_color=t["dim"])
468
+ # Column headers & containers
469
+ for key, hdr in self._column_headers.items():
470
+ hdr.configure(fg_color=t["card"])
471
+ for key, container in self._column_containers.items():
472
+ container.configure(fg_color=t["bg"])
473
+ for key, lbl in self._column_title_labels.items():
474
+ # preserve original status color but update if needed
475
+ pass
476
+ # Column scrollbars
477
+ for scroll in self._columns.values():
478
+ scroll.configure(
479
+ fg_color="transparent",
480
+ scrollbar_fg_color=t["border"],
481
+ scrollbar_button_color=t["accent"],
482
+ scrollbar_button_hover_color=t["accent_hover"],
483
+ )
484
+ self.refresh()
485
+
486
+ def destroy(self) -> None:
487
+ if self._poll_after_id:
488
+ self.after_cancel(self._poll_after_id)
489
+ super().destroy()
490
+
491
+
492
+ if __name__ == "__main__":
493
+ root = ctk.CTk()
494
+ root.title("Dulus Tasks v2")
495
+ root.geometry("1200x750")
496
+ root.configure(fg_color=BG_COLOR)
497
+ tv = TasksView(root)
498
+ tv.pack(fill="both", expand=True)
499
+ root.mainloop()