refactor: vá 4 hồi quy, tách 4 file chạm trần LOC, docstring lên 100%
Hồi quy đã vá
-------------
F-12 Kéo–thả hoặc dán tệp vào ô chat ném NameError. R08 tách `_Input` sang
`chat_input_box.py` nhưng để `_paths_from_mime()` ở lại
`composer_widget.py`, nên hai hàm sự kiện Qt gọi một cái tên không tồn
tại. Bốn hàm dùng chung chuyển sang `composer_mime.py` — module thứ ba
là chỗ duy nhất không lặp lại được lỗi này. Đo lại: cả thả lẫn dán đều
gắn 1 tệp, khớp bản trước refactor.
F-01 Đổi provider thì bộ chọn model AI-Edit không làm gì. Hook cũ kiểm
`folder.ai_model_combo`, thuộc tính R08-T12 đã dời sang
`ai_panel.resolver`. Làm mới vô điều kiện, đúng như tab cũ: lần lấy đầu
tiên hỏng thì đổi provider chính là lúc phải thử lại.
F-07 Hàng chọn kỳ của Dashboard bị đẩy xuống dưới các thẻ số liệu. Hàng này
lọc CẢ BA thẻ con chứ không riêng biểu đồ, nên để nó nằm dưới là bắt
người dùng đọc con số trước khi thấy con số đó tính cho kỳ nào. Kèm
theo: `TokenUsageCardWidget` bị bỏ sót `setContentsMargins(0,0,0,0)`
mà hai thẻ con còn lại đã có, đẩy cả hàng thẻ lệch 9px.
`check_layout_geometry` nay khớp TỪNG BYTE với bản trước refactor.
F-11 Hai lớp khai trùng tên phương thức; Python giữ bản sau nên bản đầu là
mã chết. `co4e_tab.py::showEvent` bản đầu gọi `_narrow_guard.attach()`
và không bao giờ chạy.
Tách file (F-09)
----------------
Bốn file chạm trần 400 dòng, mỗi lần cắt ra một trách nhiệm thật:
graph_renderer.py -> graph_scene_builder.py + graph_export.py
co4e_workflow_service.py -> co4e_run_history.py
json_config_repository.py -> config_sections.py
agents_admin_tab.py -> shared/agent_kind_visuals.py
File cuối còn xoá 3 bản sao của hàm đã có trong `shared/formatters.py`,
giống hệt đến từng dòng — nay định dạng thời gian và avatar không lệch nhau
giữa các bảng Giám sát nữa.
Docstring
---------
41,6% -> 100% (3.478/3.478 định nghĩa production), kể cả module dormant và
phương thức dunder. Toàn bộ phần bổ sung viết bằng tiếng Việt; comment tiếng
Anh có sẵn giữ nguyên — dịch ngược là một đợt riêng.
Seam chưa nối dây (F-05)
------------------------
9 seam mang nhãn `SEAM · dựng <ngày>` kèm hai câu: được nối khi nào, và để
dormant thì hỏng gì. Ngày lấy từ lịch sử git, không phải hạn tự đặt. Gate O
đọc nhãn đó và nhắc khi quá 30 ngày.
859 test xanh · 4/4 cổng CASAN · 19/24 checker khớp từng byte bản cũ.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,12 @@ class AiTaskCreatorDialog(QDialog):
|
||||
description, or 📥 Import from a filled Excel/CSV/JSON file."""
|
||||
|
||||
def __init__(self, ctx: AppContext, parent=None):
|
||||
"""Hộp thoại tạo task bằng AI hoặc nhập từ tệp.
|
||||
|
||||
Nhớ tab nào đang cấp danh sách task cho nút Đồng ý — hai tab cùng sinh ra
|
||||
danh sách, không phân biệt thì bấm Đồng ý có thể tạo nhầm danh sách của tab
|
||||
kia.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.ctx = ctx
|
||||
self._planner = AiTaskPlannerService(provider_factory=ctx.build_active_provider)
|
||||
@@ -82,6 +88,7 @@ class AiTaskCreatorDialog(QDialog):
|
||||
|
||||
# ---- AI-gen tab -------------------------------------------------------
|
||||
def _build_ai_gen_page(self) -> QWidget:
|
||||
"""Dựng tab "Sinh bằng AI": ô mô tả, tệp/link đính kèm và nút sinh."""
|
||||
ai_page = QWidget()
|
||||
al = QVBoxLayout(ai_page)
|
||||
al.addWidget(QLabel(tr("schedtask.ai_desc_label")))
|
||||
@@ -117,6 +124,7 @@ class AiTaskCreatorDialog(QDialog):
|
||||
return ai_page
|
||||
|
||||
def _ai_pick_files(self) -> None:
|
||||
"""Chọn tệp đính kèm làm ngữ cảnh cho AI lập kế hoạch."""
|
||||
from PySide6.QtWidgets import QFileDialog
|
||||
|
||||
files, _ = QFileDialog.getOpenFileNames(self, tr("schedtask.pick_files"))
|
||||
@@ -125,12 +133,15 @@ class AiTaskCreatorDialog(QDialog):
|
||||
self.ai_files_edit.setText("; ".join(existing + files))
|
||||
|
||||
def _attached_files(self) -> List[str]:
|
||||
"""Danh sách đường dẫn tệp đã nhập, tách theo dấu ``;``."""
|
||||
return [p.strip() for p in self.ai_files_edit.text().split(";") if p.strip()]
|
||||
|
||||
def _attached_links(self) -> List[str]:
|
||||
"""Danh sách link đã nhập, tách theo dấu ``;``."""
|
||||
return [u.strip() for u in self.ai_links_edit.text().split(";") if u.strip()]
|
||||
|
||||
def _generate(self) -> None:
|
||||
"""Nhờ AI tách mô tả thành nhiều task; đang chạy dở thì bỏ qua."""
|
||||
description = self.desc_edit.toPlainText().strip()
|
||||
if not description or self._worker is not None:
|
||||
return
|
||||
@@ -139,6 +150,7 @@ class AiTaskCreatorDialog(QDialog):
|
||||
self.gen_btn.setText(tr("schedtask.ai_generating"))
|
||||
|
||||
def job(worker: AgentWorker):
|
||||
"""Chạy nền: ghép mô tả với danh sách tệp/link rồi gọi bộ lập kế hoạch."""
|
||||
full_desc = description
|
||||
if files or links:
|
||||
attach_note = "; ".join(files + links)
|
||||
@@ -154,6 +166,7 @@ class AiTaskCreatorDialog(QDialog):
|
||||
w.start()
|
||||
|
||||
def _on_planned(self, result: dict) -> None:
|
||||
"""Có kết quả: đổ danh sách task đề xuất ra bảng để người dùng xem lại."""
|
||||
self._worker = None
|
||||
self.gen_btn.setEnabled(True)
|
||||
self.gen_btn.setText(tr("schedtask.ai_generate"))
|
||||
@@ -172,6 +185,7 @@ class AiTaskCreatorDialog(QDialog):
|
||||
self.buttons.button(QDialogButtonBox.Ok).setEnabled(bool(self._ai_planned))
|
||||
|
||||
def _on_failed(self, err: str) -> None:
|
||||
"""Lập kế hoạch lỗi: trả nút về trạng thái bấm được và hiện lý do."""
|
||||
self._worker = None
|
||||
self.gen_btn.setEnabled(True)
|
||||
self.gen_btn.setText(tr("schedtask.ai_generate"))
|
||||
@@ -179,12 +193,14 @@ class AiTaskCreatorDialog(QDialog):
|
||||
|
||||
# ---- Import tab ---------------------------------------------------------
|
||||
def _on_import_tasks_changed(self, has_tasks: bool) -> None:
|
||||
"""Tab Nhập có task thì chuyển nguồn dữ liệu sang đó và mở nút OK."""
|
||||
if has_tasks:
|
||||
self._active_source = "import"
|
||||
self.buttons.button(QDialogButtonBox.Ok).setEnabled(has_tasks)
|
||||
|
||||
# ---- confirm ------------------------------------------------------------
|
||||
def _confirm(self) -> None:
|
||||
"""Chốt: lấy task từ nguồn đang hoạt động (AI hoặc Nhập) và gắn project cho chúng."""
|
||||
planned = self.import_panel.planned if self._active_source == "import" else self._ai_planned
|
||||
project_id = self.workspace_combo.currentData() or ""
|
||||
for t in planned:
|
||||
|
||||
@@ -33,6 +33,7 @@ class _DropZone(QLabel):
|
||||
file_dropped = Signal(str)
|
||||
|
||||
def __init__(self):
|
||||
"""Vùng kéo–thả tệp danh sách task."""
|
||||
super().__init__()
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
@@ -46,12 +47,14 @@ class _DropZone(QLabel):
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
def dragEnterEvent(self, event): # noqa: N802
|
||||
"""Chỉ nhận tệp có đuôi hợp lệ."""
|
||||
urls = event.mimeData().urls()
|
||||
if urls and urls[0].toLocalFile().lower().endswith(
|
||||
(".xlsx", ".xlsm", ".xls", ".csv", ".json")):
|
||||
event.acceptProposedAction()
|
||||
|
||||
def dropEvent(self, event): # noqa: N802
|
||||
"""Thả tệp: phát đường dẫn lên để panel đọc và dựng danh sách task."""
|
||||
urls = event.mimeData().urls()
|
||||
if urls:
|
||||
self.file_dropped.emit(urls[0].toLocalFile())
|
||||
@@ -70,6 +73,7 @@ class ImportTaskPanel(QWidget):
|
||||
tasks_changed = Signal(bool) # True when the current preview has >=1 valid task
|
||||
|
||||
def __init__(self, planner: AiTaskPlannerService, parent=None):
|
||||
"""Panel nhập task từ tệp: xuất mẫu, chọn tệp, xem trước rồi mới tạo."""
|
||||
super().__init__(parent)
|
||||
self._planner = planner
|
||||
self.planned: List[dict] = []
|
||||
@@ -96,6 +100,7 @@ class ImportTaskPanel(QWidget):
|
||||
il.addWidget(self.preview, 1)
|
||||
|
||||
def _export_template(self) -> None:
|
||||
"""Xuất file mẫu Excel để người dùng điền danh sách task rồi nhập lại."""
|
||||
from PySide6.QtWidgets import QFileDialog
|
||||
|
||||
from cowork_local.core.task_excel import export_template
|
||||
@@ -112,6 +117,7 @@ class ImportTaskPanel(QWidget):
|
||||
QMessageBox.warning(self, tr("schedtask.tab_import"), str(exc))
|
||||
|
||||
def _pick_import_file(self) -> None:
|
||||
"""Chọn file danh sách task để nhập."""
|
||||
from PySide6.QtWidgets import QFileDialog
|
||||
|
||||
from cowork_local.core.task_import import IMPORT_FILTER
|
||||
@@ -122,6 +128,9 @@ class ImportTaskPanel(QWidget):
|
||||
self._load_import_file(path)
|
||||
|
||||
def _load_import_file(self, path: str) -> None:
|
||||
"""Đọc file và dựng danh sách task xem trước; file sai khuôn thì hiện lý do cụ
|
||||
thể chứ không im lặng bỏ qua.
|
||||
"""
|
||||
try:
|
||||
self.planned = self._planner.import_file(path)
|
||||
except ValueError as exc:
|
||||
|
||||
@@ -28,10 +28,12 @@ _WEEKDAY_KEYS = ("mon", "tue", "wed", "thu", "fri", "sat", "sun")
|
||||
|
||||
|
||||
class _DayCell(QFrame):
|
||||
"""Một ô ngày trên lưới lịch: số ngày, nút thêm task, và danh sách task của ngày đó."""
|
||||
add_requested = Signal(str) # "YYYY-MM-DD"
|
||||
task_clicked = Signal(str) # task_id
|
||||
|
||||
def __init__(self):
|
||||
"""Một ô ngày trên lịch, chứa các task đến hạn hôm đó."""
|
||||
super().__init__()
|
||||
self.setObjectName("dayCell")
|
||||
self.setFrameShape(QFrame.StyledPanel)
|
||||
@@ -56,6 +58,12 @@ class _DayCell(QFrame):
|
||||
|
||||
def set_day(self, d: date, tasks: List[dict], dim: bool,
|
||||
today: bool = False, weekend: bool = False) -> None:
|
||||
"""Vẽ lại ô cho một ngày cụ thể.
|
||||
|
||||
Chỉ "hôm nay" được nền tô đậm kèm viền nhấn; cuối tuần chỉ đổi nền chìm.
|
||||
Nhờ vậy mắt bắt vào hôm nay trước, còn khối cuối tuần chỉ hiện ra khi
|
||||
người dùng quét cả tháng. ``dim`` làm mờ ngày thuộc tháng khác.
|
||||
"""
|
||||
self._date_str = d.isoformat()
|
||||
self.date_lbl.setText(str(d.day))
|
||||
p = current_palette()
|
||||
@@ -81,16 +89,23 @@ class _DayCell(QFrame):
|
||||
self.list.addItem(item)
|
||||
|
||||
def _on_item_clicked(self, item: QListWidgetItem) -> None:
|
||||
"""Bấm vào một task trong ô: phát id lên để lớp trên mở trình sửa."""
|
||||
tid = item.data(Qt.UserRole)
|
||||
if tid:
|
||||
self.task_clicked.emit(tid)
|
||||
|
||||
|
||||
class CalendarView(QWidget):
|
||||
"""Khung nhìn Lịch của màn Lịch trình: xem theo tuần, tháng hoặc năm.
|
||||
|
||||
Widget này chỉ VẼ và phát tín hiệu; việc tạo/sửa task do vỏ
|
||||
``ScheduleTaskTab`` làm, vì Kanban cũng cần đúng hành vi đó.
|
||||
"""
|
||||
add_task_on_date = Signal(str) # "YYYY-MM-DD"
|
||||
edit_task = Signal(str) # task_id
|
||||
|
||||
def __init__(self):
|
||||
"""Lịch xem task theo tháng/tuần/ngày, neo vào hôm nay."""
|
||||
super().__init__()
|
||||
self.granularity = "month"
|
||||
self.anchor = date.today()
|
||||
@@ -131,6 +146,7 @@ class CalendarView(QWidget):
|
||||
self._retranslate()
|
||||
|
||||
def _retranslate(self) -> None:
|
||||
"""Áp lại chữ theo ngôn ngữ đang chọn rồi vẽ lại lưới."""
|
||||
self.today_btn.setText(tr("schedtask.cal_today"))
|
||||
self.prev_btn.setToolTip(tr("schedtask.cal_prev"))
|
||||
self.next_btn.setToolTip(tr("schedtask.cal_next"))
|
||||
@@ -140,6 +156,7 @@ class CalendarView(QWidget):
|
||||
|
||||
# ---- public ------------------------------------------------------
|
||||
def set_tasks(self, tasks: List[dict]) -> None:
|
||||
"""Nhận danh sách task mới và vẽ lại toàn bộ khung nhìn."""
|
||||
self._tasks = tasks
|
||||
self._render()
|
||||
|
||||
@@ -157,14 +174,17 @@ class CalendarView(QWidget):
|
||||
|
||||
# ---- navigation ---------------------------------------------------
|
||||
def _shift(self, direction: int) -> None:
|
||||
"""Lùi/tiến một kỳ theo đúng độ mịn đang chọn (tuần, tháng hay năm)."""
|
||||
self.anchor = shift_period(self.anchor, self.granularity, direction)
|
||||
self._render()
|
||||
|
||||
def _go_today(self) -> None:
|
||||
"""Nhảy về kỳ chứa ngày hôm nay."""
|
||||
self.anchor = date.today()
|
||||
self._render()
|
||||
|
||||
def _on_granularity_changed(self) -> None:
|
||||
"""Đổi độ mịn tuần/tháng/năm và vẽ lại."""
|
||||
data = self.granularity_combo.currentData()
|
||||
if data:
|
||||
self.granularity = data
|
||||
@@ -172,6 +192,11 @@ class CalendarView(QWidget):
|
||||
|
||||
# ---- rendering ------------------------------------------------------
|
||||
def _clear_grid(self) -> None:
|
||||
"""Xoá sạch lưới trước khi vẽ lại.
|
||||
|
||||
Dùng ``deleteLater`` chứ không bỏ tham chiếu: widget Qt còn đang trong
|
||||
hàng đợi sự kiện, xoá ngay có thể làm vỡ lúc đang xử lý sự kiện chuột.
|
||||
"""
|
||||
while self._grid.count():
|
||||
item = self._grid.takeAt(0)
|
||||
w = item.widget()
|
||||
@@ -179,6 +204,7 @@ class CalendarView(QWidget):
|
||||
w.deleteLater()
|
||||
|
||||
def _render(self) -> None:
|
||||
"""Vẽ lại lưới theo độ mịn đang chọn: tuần, tháng (kèm ngày tràn) hoặc năm."""
|
||||
self._update_period_label()
|
||||
self._clear_grid()
|
||||
by_date = group_tasks_by_date(self._tasks)
|
||||
@@ -191,6 +217,11 @@ class CalendarView(QWidget):
|
||||
|
||||
def _render_days(self, days: List[date], by_date: Dict[str, List[dict]],
|
||||
mark_month: Optional[int] = None) -> None:
|
||||
"""Vẽ lưới 7 cột: hàng đầu là tên thứ, các hàng sau là ô ngày.
|
||||
|
||||
``mark_month`` là tháng "chính": ngày thuộc tháng khác bị làm mờ. Cột 5 và
|
||||
6 là thứ Bảy và Chủ nhật vì ``_WEEKDAY_KEYS`` bắt đầu từ thứ Hai.
|
||||
"""
|
||||
for col, key in enumerate(_WEEKDAY_KEYS):
|
||||
lbl = QLabel(tr(f"schedtask.cal_weekday.{key}"))
|
||||
lbl.setStyleSheet("font-weight:600;")
|
||||
@@ -210,6 +241,7 @@ class CalendarView(QWidget):
|
||||
self._grid.addWidget(cell, r, c)
|
||||
|
||||
def _render_year(self, by_date: Dict[str, List[dict]]) -> None:
|
||||
"""Xem theo năm: danh sách 12 tháng kèm số task, bấm vào là mở tháng đó."""
|
||||
counts = month_task_counts(by_date, self.anchor.year)
|
||||
lst = QListWidget()
|
||||
for m in range(1, 13):
|
||||
@@ -223,6 +255,7 @@ class CalendarView(QWidget):
|
||||
self._grid.addWidget(lst, 0, 0)
|
||||
|
||||
def _update_period_label(self) -> None:
|
||||
"""Cập nhật nhãn kỳ đang xem: khoảng ngày (tuần), năm, hoặc YYYY-MM (tháng)."""
|
||||
if self.granularity == "week":
|
||||
days = week_days(self.anchor)
|
||||
self.period_lbl.setText(f"{days[0].isoformat()} - {days[-1].isoformat()}")
|
||||
|
||||
@@ -52,6 +52,11 @@ class _KanbanColumn(QListWidget):
|
||||
task_dropped = Signal(str, str) # task_id, new_status
|
||||
|
||||
def __init__(self, status: str):
|
||||
"""Một cột Kanban ứng với một trạng thái task.
|
||||
|
||||
Cho chọn nhiều thẻ trong CÙNG một cột (Shift/Ctrl) để chuột phải xoá hàng
|
||||
loạt, thay vì xoá từng cái.
|
||||
"""
|
||||
super().__init__()
|
||||
self.status = status
|
||||
self.setDragDropMode(QAbstractItemView.DragDrop)
|
||||
@@ -68,6 +73,7 @@ class _KanbanColumn(QListWidget):
|
||||
self.setResizeMode(QListWidget.Adjust) # re-wrap on every resize
|
||||
|
||||
def dropEvent(self, event): # noqa: N802
|
||||
"""Thả một thẻ từ lane khác sang: báo lên bảng để đổi trạng thái task."""
|
||||
source = event.source()
|
||||
if isinstance(source, _KanbanColumn) and source is not self:
|
||||
item = source.currentItem()
|
||||
@@ -106,6 +112,11 @@ class KanbanBoardWidget(QWidget):
|
||||
|
||||
def __init__(self, ctx, tasks_dir=None, scheduler=None,
|
||||
service: Optional[TaskApplicationService] = None, parent=None):
|
||||
"""Bảng Kanban các task.
|
||||
|
||||
``service`` để None thì tự dựng một cái từ kho task; ``scheduler`` để None
|
||||
thì bảng chỉ đọc/ghi task chứ không chạy được cái nào.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.ctx = ctx
|
||||
self._tasks_dir = tasks_dir
|
||||
@@ -148,16 +159,23 @@ class KanbanBoardWidget(QWidget):
|
||||
scroll.viewport().installEventFilter(self)
|
||||
|
||||
def retranslate(self) -> None:
|
||||
"""Áp lại tooltip của từng lane theo ngôn ngữ đang chọn."""
|
||||
for status, col in self.columns.items():
|
||||
col.setToolTip(tr(f"schedtask.col_tip.{status}"))
|
||||
|
||||
# ---- lane widths ------------------------------------------------------
|
||||
def eventFilter(self, obj, event): # noqa: N802
|
||||
"""Vùng cuộn đổi kích thước thì chia lại bề rộng cho 7 lane."""
|
||||
if obj is self._board_scroll.viewport() and event.type() == QEvent.Resize:
|
||||
self._fit_lanes()
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
def _fit_lanes(self) -> None:
|
||||
"""Chia bề rộng cho 7 lane sao cho tất cả vừa một màn.
|
||||
|
||||
Có sàn tối thiểu tính theo bề rộng ký tự, để tiêu đề lane không bị cắt cụt
|
||||
khi cửa sổ hẹp.
|
||||
"""
|
||||
floor = self.fontMetrics().averageCharWidth() * self._LANE_FLOOR_CH + 24
|
||||
for col in self.columns.values():
|
||||
if col.minimumWidth() != floor:
|
||||
@@ -165,6 +183,7 @@ class KanbanBoardWidget(QWidget):
|
||||
|
||||
# ---- rendering ----------------------------------------------------------
|
||||
def _card_text(self, t: dict) -> str:
|
||||
"""Chuỗi hiển thị trên một thẻ task: tiêu đề kèm mốc thời gian/độ ưu tiên."""
|
||||
prio = _PRIORITY_ICONS.get(t.get("priority", "medium"), "")
|
||||
ai = "[AI] " if t.get("is_ai_generated") else ""
|
||||
sched = t.get("schedule", {})
|
||||
@@ -218,6 +237,7 @@ class KanbanBoardWidget(QWidget):
|
||||
|
||||
# ---- actions --------------------------------------------------------
|
||||
def _on_double_click(self, item: QListWidgetItem) -> None:
|
||||
"""Bấm đúp một thẻ: mở trình sửa task đó."""
|
||||
tid = item.data(Qt.UserRole)
|
||||
if tid:
|
||||
self.edit_requested.emit(tid)
|
||||
@@ -252,6 +272,10 @@ class KanbanBoardWidget(QWidget):
|
||||
return len(selected) > 1 and item in selected
|
||||
|
||||
def _context_menu(self, col: _KanbanColumn, pos) -> None:
|
||||
"""Menu chuột phải trên một thẻ: chạy ngay, xem log, tạo task tiếp theo, xoá.
|
||||
|
||||
Đang chọn nhiều thẻ thì chuyển sang menu xoá hàng loạt.
|
||||
"""
|
||||
item = col.itemAt(pos)
|
||||
if item is None or not item.data(Qt.UserRole):
|
||||
return
|
||||
@@ -300,6 +324,7 @@ class KanbanBoardWidget(QWidget):
|
||||
self.refresh()
|
||||
|
||||
def _bulk_delete_menu(self, col: _KanbanColumn, pos, selected) -> None:
|
||||
"""Menu xoá hàng loạt khi đang chọn nhiều thẻ."""
|
||||
menu = QMenu(col)
|
||||
del_act = menu.addAction(tr("schedtask.menu_delete_selected", n=len(selected)))
|
||||
chosen = menu.exec(col.viewport().mapToGlobal(pos))
|
||||
@@ -320,6 +345,7 @@ class KanbanBoardWidget(QWidget):
|
||||
return True
|
||||
|
||||
def _emit_run_now_message(self, result, title: str = "") -> None:
|
||||
"""Báo kết quả của lệnh "chạy ngay" ra thanh trạng thái."""
|
||||
if result is None:
|
||||
return
|
||||
if result.ok:
|
||||
@@ -330,6 +356,7 @@ class KanbanBoardWidget(QWidget):
|
||||
self.status_message.emit(tr("schedtask.msg_no_scheduler"))
|
||||
|
||||
def _view_logs(self, task: dict) -> None:
|
||||
"""Mở thư mục hiện vật chứa log của các lượt chạy task này."""
|
||||
from cowork_local.core.tasks import ARTIFACTS_DIR
|
||||
|
||||
run_id = task.get("logs", {}).get("last_run_id")
|
||||
|
||||
@@ -18,6 +18,7 @@ class RunHistoryDialog(QDialog):
|
||||
double-click a row to open that run's artifact folder."""
|
||||
|
||||
def __init__(self, task: dict, parent=None):
|
||||
"""Hộp thoại xem lịch sử các lần chạy của một task."""
|
||||
super().__init__(parent)
|
||||
self._task = task
|
||||
self.setWindowTitle(f"{tr('schedtask.menu_history')} — {task.get('title', '')}")
|
||||
@@ -58,6 +59,7 @@ class RunHistoryDialog(QDialog):
|
||||
root.addWidget(buttons)
|
||||
|
||||
def _open_artifact(self, item: QTableWidgetItem) -> None:
|
||||
"""Bấm một dòng: mở thư mục hiện vật của lượt chạy đó."""
|
||||
from cowork_local.core.tasks import ARTIFACTS_DIR
|
||||
|
||||
first = self.table.item(item.row(), 0)
|
||||
|
||||
@@ -33,9 +33,22 @@ _VIEWS = ("kanban", "calendar")
|
||||
|
||||
|
||||
class ScheduleTaskTab(QWidget):
|
||||
"""Màn Lịch trình: phần vỏ ghép bảng Kanban và lịch tháng.
|
||||
|
||||
Vỏ giữ ba việc mà cả hai khung nhìn đều cần: thanh tiêu đề, nút chuyển
|
||||
khung nhìn, và mở hộp thoại sửa task. Việc sửa task nằm ở đây (không ở
|
||||
từng widget con) vì Kanban lẫn Lịch đều cần đúng một hành vi "mở trình
|
||||
sửa cho task id này" — cả hai phát tín hiệu, vỏ mở hộp thoại.
|
||||
"""
|
||||
status_message = Signal(str)
|
||||
|
||||
def __init__(self, ctx: AppContext, scheduler=None, tasks_dir: Optional[Path] = None):
|
||||
"""Vỏ màn Lịch task, ghép bảng Kanban và lịch.
|
||||
|
||||
``tasks_dir`` để None thì dùng thư mục mặc định. Cho phép truyền vào là điểm
|
||||
mới của R08-T11 — bản gộp cũ viết cứng, nên không test được nếu không đụng
|
||||
thư mục cấu hình thật của người dùng.
|
||||
"""
|
||||
super().__init__()
|
||||
self.ctx = ctx
|
||||
self.scheduler = scheduler # TaskScheduler (may be None in tests)
|
||||
@@ -110,6 +123,7 @@ class ScheduleTaskTab(QWidget):
|
||||
|
||||
# ---- i18n ------------------------------------------------------------
|
||||
def _retranslate(self) -> None:
|
||||
"""Áp lại chữ theo ngôn ngữ đang chọn cho tiêu đề, nút và tên hai khung nhìn."""
|
||||
self._title.setText(tr("schedtask.title"))
|
||||
self.add_btn.setText(tr("schedtask.add_btn"))
|
||||
self.add_btn.setToolTip(tr("schedtask.add_tooltip"))
|
||||
@@ -122,24 +136,33 @@ class ScheduleTaskTab(QWidget):
|
||||
|
||||
# ---- Kanban / Calendar view switch --------------------------------
|
||||
def _on_view_changed(self) -> None:
|
||||
"""Đổi khung nhìn Kanban ⇄ Lịch theo tab đang chọn."""
|
||||
self._view_stack.setCurrentIndex(self.view_tabs.currentIndex())
|
||||
|
||||
def _on_counts_changed(self, counts: dict) -> None:
|
||||
"""Cập nhật dòng tóm tắt số task theo trạng thái.
|
||||
|
||||
Chỉ liệt kê trạng thái có task; đặt luôn tooltip để khi thanh bị co hẹp
|
||||
người dùng vẫn đọc được đầy đủ.
|
||||
"""
|
||||
summary = " ".join(
|
||||
f"{tr(f'schedtask.status.{s}')}: {counts[s]}" for s in STATUSES if counts[s])
|
||||
self.counts_lbl.setText(summary)
|
||||
self.counts_lbl.setToolTip(summary) # full text stays reachable if clipped
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""Đọc lại toàn bộ task và vẽ lại cả Kanban lẫn Lịch."""
|
||||
all_tasks = self.kanban.refresh()
|
||||
self.calendar.set_tasks(all_tasks)
|
||||
|
||||
# ---- task creation / editing (shared by Kanban + Calendar) -----------
|
||||
def _save_and_refresh(self, task: dict) -> None:
|
||||
"""Ghi task xuống đĩa rồi vẽ lại màn hình."""
|
||||
save_task(task, self._tasks_dir)
|
||||
self.refresh()
|
||||
|
||||
def _add_task(self) -> None:
|
||||
"""Mở trình sửa để tạo task mới; chỉ lưu khi người dùng bấm xác nhận."""
|
||||
from cowork_local.ui.task_editor_dialog import TaskEditorDialog
|
||||
|
||||
dlg = TaskEditorDialog(None, list_tasks(self._tasks_dir), self, ctx=self.ctx)
|
||||
@@ -148,6 +171,9 @@ class ScheduleTaskTab(QWidget):
|
||||
self.status_message.emit(tr("schedtask.msg_created"))
|
||||
|
||||
def _edit_task(self, task_id: str) -> None:
|
||||
"""Mở trình sửa cho một task đã có. Không tìm thấy id thì bỏ qua lặng lẽ
|
||||
(task có thể vừa bị xoá ở khung nhìn khác).
|
||||
"""
|
||||
from cowork_local.ui.task_editor_dialog import TaskEditorDialog
|
||||
|
||||
task = load_task(task_id, self._tasks_dir)
|
||||
@@ -170,6 +196,7 @@ class ScheduleTaskTab(QWidget):
|
||||
|
||||
# ---- AI create ----------------------------------------------------------
|
||||
def _ai_create(self) -> None:
|
||||
"""Mở hộp thoại nhờ AI lập kế hoạch, rồi lưu toàn bộ task nó sinh ra."""
|
||||
from cowork_local.presentation.scheduling.ai_task_creator_dialog import (
|
||||
AiTaskCreatorDialog,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user