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:
2026-08-30 10:41:45 +09:00
co-authored by Claude Opus 5
parent d20306be08
commit e29a0ccdbd
264 changed files with 4593 additions and 359 deletions
@@ -49,6 +49,11 @@ class _CanvasInteractionMixin:
# ---- bottom-left overlay (zoom / fit) --------------------------------
def add_overlay(self, widget) -> None:
"""Gắn thanh điều khiển zoom/fit vào góc dưới-trái khung nhìn.
Widget được đặt làm con của ``viewport()`` chứ không của khung vẽ, nên nó
đứng yên khi người dùng kéo hay phóng to đồ thị.
"""
self._overlay = widget
widget.setParent(self.viewport())
widget.show()
@@ -56,6 +61,7 @@ class _CanvasInteractionMixin:
self._place_overlay()
def _place_overlay(self) -> None:
"""Ghim lại thanh điều khiển vào góc dưới-trái, cách lề 12px."""
if self._overlay is not None:
self._overlay.adjustSize()
vp = self.viewport()
@@ -63,10 +69,16 @@ class _CanvasInteractionMixin:
self._overlay.raise_()
def resizeEvent(self, e): # noqa: N802
"""Đổi kích thước khung thì ghim lại thanh điều khiển."""
super().resizeEvent(e)
self._place_overlay()
def scrollContentsBy(self, dx, dy): # noqa: N802
"""Cuộn khung thì ghim lại thanh điều khiển.
``QGraphicsView`` cuộn cả widget con của viewport theo cảnh, nên không
ghim lại thì nút +/−/fit sẽ trôi khỏi góc.
"""
# QGraphicsView scrolls the viewport's child widgets along with the
# scene, so panning/scrolling would drag the zoom overlay off-corner.
# Re-pin it after every scroll so +/−/fit stay fixed in place.
@@ -74,11 +86,19 @@ class _CanvasInteractionMixin:
self._place_overlay()
def showEvent(self, e): # noqa: N802
"""Lần hiện đầu tiên mới biết kích thước thật của viewport — ghim lại lúc đó."""
super().showEvent(e)
self._place_overlay() # viewport size is final once shown
# ---- zoom / fit -------------------------------------------------------
def _zoom_by(self, factor: float) -> None:
"""Phóng to/thu nhỏ theo hệ số, chặn trong khoảng ``_ZOOM_MIN``..``_ZOOM_MAX``.
Mức phóng hiện tại đọc thẳng từ ma trận biến đổi, KHÔNG dùng biến đếm
riêng: biến đếm lệch khỏi thực tế sau mỗi lần ``fit_view``/``relayout``/
``reset_zoom`` là đúng thứ từng làm nút +/− và Ctrl+lăn ngẫu nhiên chết.
Chặn ở đích rồi mới tính hệ số cần áp, nên vẫn phóng được sát mép giới hạn.
"""
# Derive the CURRENT scale from the live transform (never a separate
# accumulator that can drift out of sync with fit_view/relayout/reset —
# that drift is what made the +/− buttons and Ctrl+wheel randomly stop
@@ -92,16 +112,22 @@ class _CanvasInteractionMixin:
self._zoom = target
def zoom_in(self) -> None:
"""Phóng to một nấc (×1,15)."""
self._zoom_by(1.15)
def zoom_out(self) -> None:
"""Thu nhỏ một nấc (÷1,15)."""
self._zoom_by(1 / 1.15)
def reset_zoom(self) -> None:
"""Về đúng tỉ lệ 1:1 và bỏ mọi dịch chuyển."""
self.resetTransform()
self._zoom = 1.0
def wheelEvent(self, e):
"""Lăn chuột: Ctrl = phóng to/thu nhỏ quanh con trỏ, Shift = cuộn ngang,
không phím = cuộn dọc như mặc định.
"""
# Ctrl+wheel = zoom (anchored under the cursor); Shift+wheel = pan
# horizontally; plain wheel scrolls vertically.
if e.modifiers() & Qt.ControlModifier:
@@ -117,6 +143,7 @@ class _CanvasInteractionMixin:
# ---- middle-mouse drag-to-pan ----------------------------------------
def mousePressEvent(self, e):
"""Nhấn chuột giữa: bắt đầu kéo cả khung (pan), đổi con trỏ thành bàn tay nắm."""
if e.button() == Qt.MiddleButton:
self._panning = True
self._pan_start = e.position().toPoint()
@@ -126,6 +153,7 @@ class _CanvasInteractionMixin:
super().mousePressEvent(e)
def mouseMoveEvent(self, e):
"""Đang kéo khung: dời hai thanh cuộn ngược chiều con trỏ."""
if self._panning and self._pan_start is not None:
pos = e.position().toPoint()
delta = pos - self._pan_start
@@ -137,6 +165,7 @@ class _CanvasInteractionMixin:
super().mouseMoveEvent(e)
def mouseReleaseEvent(self, e):
"""Thả chuột giữa: kết thúc kéo khung, trả con trỏ về bình thường."""
if e.button() == Qt.MiddleButton and self._panning:
self._panning = False
self.setCursor(Qt.ArrowCursor)
@@ -208,6 +237,9 @@ class _CanvasInteractionMixin:
# ---- key / drop -------------------------------------------------------
def keyPressEvent(self, e):
"""Phím tắt: Delete/Backspace xoá phần đang chọn, Esc huỷ lượt nối đang dở,
Ctrl +/−/0 phóng to/thu nhỏ/về 1:1.
"""
if e.key() in (Qt.Key_Delete, Qt.Key_Backspace):
self.delete_selected()
return
@@ -227,18 +259,25 @@ class _CanvasInteractionMixin:
super().keyPressEvent(e)
def dragEnterEvent(self, e):
"""Chỉ nhận thứ kéo vào mang đúng định dạng của bảng nguyên liệu Co4E."""
if e.mimeData().hasFormat(CO4E_MIME):
e.acceptProposedAction()
else:
super().dragEnterEvent(e)
def dragMoveEvent(self, e):
"""Giữ trạng thái nhận thả trong suốt lúc rê qua khung."""
if e.mimeData().hasFormat(CO4E_MIME):
e.acceptProposedAction()
else:
super().dragMoveEvent(e)
def dropEvent(self, e):
"""Thả một bước hoặc cả một luồng từ bảng nguyên liệu xuống đúng vị trí con trỏ.
Payload hỏng thì bỏ qua lặng lẽ — thả nhầm thứ gì đó vào khung không
được phép làm vỡ màn hình.
"""
if not e.mimeData().hasFormat(CO4E_MIME):
super().dropEvent(e)
return