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/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()
|