"""Lịch sử các lượt chạy của một task — R08-T11. Mở từ menu chuột phải trên thẻ Kanban. Chỉ đọc: liệt kê từng lượt đã chạy, kết quả và log. """ from __future__ import annotations import copy from pathlib import Path from typing import Dict, List, Optional from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import ( QAbstractItemView, QComboBox, QDialog, QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, QMenu, QMessageBox, QPlainTextEdit, QPushButton, QScrollArea, QSizePolicy, QStackedWidget, QTabBar, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget, ) from ...core import tasks as taskrepo from ...core.projects import list_projects from ...core.tasks import STATUSES, chain_error, duplicate_task, new_task from ...core.worker import AgentWorker from ...i18n import on_language_changed, tr from ...state import AppContext from ...theme import current_palette from ...ui.calendar_view import CalendarView from ...ui.icons import icon from ...ui.osutil import open_path class _RunHistoryDialog(QDialog): """Run history of one task as a table (newest first): time, status, error; double-click a row to open that run's artifact folder.""" def __init__(self, task: dict, parent=None): super().__init__(parent) self._task = task self.setWindowTitle(f"{tr('schedtask.menu_history')} — {task.get('title', '')}") self.resize(620, 380) root = QVBoxLayout(self) hint = QLabel(tr("schedtask.hist_hint")) hint.setObjectName("hint") root.addWidget(hint) runs = list(reversed(task.get("runs", []) or [])) self.table = QTableWidget(len(runs), 4) self.table.setHorizontalHeaderLabels([ tr("schedtask.hist_col_time"), tr("schedtask.hist_col_status"), tr("schedtask.hist_col_run"), tr("schedtask.hist_col_error"), ]) self.table.setEditTriggers(QAbstractItemView.NoEditTriggers) self.table.setSelectionBehavior(QAbstractItemView.SelectRows) for row, run in enumerate(runs): ok = run.get("status") == "success" cells = ( run.get("finished_at", ""), str(run.get("status", "")), run.get("run_id", ""), (run.get("error") or "")[:200], ) for col, text in enumerate(cells): item = QTableWidgetItem(str(text)) if col == 0: item.setData(Qt.UserRole, run.get("run_id", "")) self.table.setItem(row, col, item) self.table.resizeColumnsToContents() self.table.horizontalHeader().setStretchLastSection(True) self.table.itemDoubleClicked.connect(self._open_artifact) root.addWidget(self.table, 1) buttons = QDialogButtonBox(QDialogButtonBox.Close) buttons.rejected.connect(self.reject) buttons.accepted.connect(self.accept) root.addWidget(buttons) def _open_artifact(self, item: QTableWidgetItem) -> None: first = self.table.item(item.row(), 0) run_id = first.data(Qt.UserRole) if first else "" if not run_id: return folder = taskrepo.ARTIFACTS_DIR / self._task["task_id"] / run_id if folder.exists(): open_path(str(folder))