fix(qa): resolve DF-002 through DF-011 from QA defect tracking sheet

Batch of fixes for defects tracked in "Task Tracking Template.xlsx" (sheet
Defect Management), verified against the sheet's Root Cause/Cach xu ly
columns before this commit:

- DF-002: Co4E node status not reflected after tab switch + missing
  edit-lock on running/done nodes (node_property_panel.py, co4e_runs.py,
  co4e_workflow_crud.py, co4e_canvas_widget.py, co4e_flow_tabs.py,
  canvas_items.py)
- DF-003: hide the run.bat console window unless the app exits with an
  error (run.bat, scripts/console_visibility.ps1 - new)
- DF-004: floating Help Assistant icon covering the Send button after a
  window resize (presentation/shell/main_window.py)
- DF-005: "block network" toggle didn't stop ICMP/raw-socket tools like
  ping (infrastructure/filesystem/command_tools.py,
  security/command_risk_classifier.py)
- DF-006: Monitoring "gay nang khi log lon" - root cause was re-reading
  the ENTIRE audit log history every 3s tick, not missing pagination;
  bounded to a 30-day window (presentation/monitoring/monitoring_tab.py)
  AND added the "So dong/trang" page-size control the ticket also asked
  for (presentation/monitoring/shared/event_table.py,
  shared/filter_scaffold.py, tabs/action_logs_tab.py, tabs/mcp_tab.py,
  tabs/security_events_tab.py, i18n/agents_admin_tab.py)
- DF-007: support choosing a OneDrive/SharePoint folder as a project's
  working directory via Microsoft Graph, downloaded as a local mirror
  with manual sync (core/projects.py, core/ms365_graph.py,
  core/cloud_workspace_sync.py - new, ui/ms365_signin_dialog.py - new,
  ui/cloud_folder_picker_dialog.py - new, i18n/cloud_workspace.py - new,
  ui/workspace_tab.py)
- DF-008: AI-edit instruction box was a fixed-height single-line QLineEdit;
  replaced with an auto-expanding, Enter-to-send/Shift+Enter-newline input
  (presentation/folder/ai_file_editor_dialog.py)
- DF-011: run_command failed with WinError 267 for a project whose
  per-turn output directory had never been created
  (application/conversations/core_runtime_adapter.py)

DF-009 (AI-edit Apply/Discard buttons easy to miss) and DF-010 (AI reply
language - dev-confirmed not a bug) are intentionally NOT part of this
commit: DF-009 has no code fix yet (still "Assigned" in the sheet, only a
UX recommendation was recorded), DF-010 was rejected as expected behavior.

Tests: tests/test_cloud_workspace_sync.py, tests/test_ms365_cloud_dialogs.py,
tests/test_ai_file_editor_input.py, tests/test_monitoring_page_size.py (all
new, all passing). Full suite: 896 passed, 13 known-and-documented failures
unrelated to this change (an existing core/audit_log.py bug, this checkout
not being a git repo before now, and a repo/subprocess folder-naming
mismatch affecting ~66 characterization tests) - see the sheet's DF-006
Evidence column for details.
This commit is contained in:
2026-09-07 21:22:00 +09:00
parent e5fa21ecfd
commit 2a5ee29c2c
33 changed files with 1404 additions and 25 deletions
+8
View File
@@ -199,6 +199,14 @@ class _NodeItem(QGraphicsObject):
e.accept()
return
super().mousePressEvent(e)
if self.isSelected():
# itemChange() only emits node_selected when the SELECTION STATE
# actually flips (ItemSelectedHasChanged) — clicking a node that
# was already selected (e.g. left selected when a run started)
# never re-fires it, so the property panel silently kept showing
# stale data and looked "locked" while the node ran. Emit
# explicitly on every click so the panel always reloads.
self.canvas.node_selected.emit(self.node.id)
def mouseMoveEvent(self, e):
"""Rê chuột trong lúc kéo nối: vẽ lại đường nét đứt theo con trỏ."""
+9
View File
@@ -273,6 +273,15 @@ class Co4ECanvas(_CanvasInteractionMixin, QGraphicsView):
item.status = status
item.update()
def node_status(self, node_id: str) -> str:
"""Trạng thái chạy hiện tại của một node — "idle" nếu không tìm thấy.
Dùng để quyết định có khóa bảng thuộc tính bên phải hay không khi
người dùng chọn node (xem ``StepConfigPanel.set_locked``).
"""
item = self._nodes.get(node_id)
return item.status if item is not None else "idle"
def reset_statuses(self) -> None:
"""Đưa mọi node về trạng thái chờ — gọi trước mỗi lần chạy lại luồng."""
for it in self._nodes.values():
+5
View File
@@ -101,6 +101,11 @@ class Co4EFlowTabsMixin:
self.center_stack.setCurrentIndex(1)
self._sync_runs_toggle(False)
self._apply_workflow(self._flows[flow_idx])
# _apply_workflow() rebuilds the canvas from wf.nodes/edges, which
# resets every node's live status to "idle" — without this, coming
# back to a flow that's still running (e.g. from the Runs page)
# shows every node as idle even though it's actually mid-run.
self._reflect_active_run(self._flows[flow_idx].id)
def _sync_runs_toggle(self, on: bool) -> None:
"""Keep the Runs toggle showing which page is up, however it got there
(a double-click in the runs table also switches pages)."""
+9 -1
View File
@@ -17,6 +17,7 @@ from PySide6.QtWidgets import QInputDialog, QMenu, QMessageBox, QTableWidget, QT
from ...core import co4e
from ...i18n import tr
from ...theme import current_palette
from .co4e_workflow_crud import _LOCKED_NODE_STATUSES
class Co4ERunsMixin:
@@ -155,7 +156,14 @@ class Co4ERunsMixin:
t = ev.get("type")
if t == "node_status":
if shown:
self.canvas.update_node_status(ev.get("node_id"), ev.get("status"))
nid = ev.get("node_id")
self.canvas.update_node_status(nid, ev.get("status"))
# If the panel is showing THIS node right now (e.g. it was
# idle and the user had it open when the run started), keep
# the lock in sync instead of waiting for the next click.
if nid == getattr(self.config, "_node_id", None):
self.config.set_locked(
self.canvas.node_status(nid) in _LOCKED_NODE_STATUSES)
elif t == "node_output":
if run_wf is not None:
self._outputs_for(run_wf)[ev["node_id"]] = ev.get("output", "")
+9 -1
View File
@@ -10,10 +10,13 @@ from typing import List, Optional
from PySide6.QtCore import QSize, Qt
from PySide6.QtWidgets import QInputDialog, QMenu
from ...core import co4e
from ...core.co4e import STEP_DONE, STEP_RUNNING
from ...i18n import tr
from ...ui.icons import icon
from ...presentation.co4e.co4e_chat_view import _skill_names
_LOCKED_NODE_STATUSES = (STEP_RUNNING, STEP_DONE)
class Co4EWorkflowCrudMixin:
"""Phần tạo/mở/lưu/xoá luồng của Co4E Studio.
@@ -163,10 +166,15 @@ class Co4EWorkflowCrudMixin:
self.canvas.add_palette_step(co4e.Step(label="New Step"),
self.canvas.mapToScene(self.canvas.rect().center()))
def _on_node_selected(self, node_id: str) -> None:
"""Chọn một node thì nạp bước đó vào bảng thuộc tính, tự mở bảng nếu đang gập."""
"""Chọn một node thì nạp bước đó vào bảng thuộc tính, tự mở bảng nếu đang gập.
Bước đang chạy hoặc đã chạy xong thì khoá ô nhập liệu ngay khi nạp —
tránh sửa nhầm cấu hình của lần chạy đang xem kết quả.
"""
for n in self.canvas.nodes():
if n.id == node_id:
self.config.load_step(node_id, n.data, _skill_names())
self.config.set_locked(self.canvas.node_status(node_id) in _LOCKED_NODE_STATUSES)
if self._config_collapsed:
self._toggle_config()
return
+24 -1
View File
@@ -38,7 +38,7 @@ from PySide6.QtWidgets import (
)
from ...config import PROVIDER_LABELS
from ...core.co4e import PERMISSION_PRESETS, Step
from ...core.co4e import PERMISSION_PRESETS, STEP_DONE, STEP_RUNNING, Step
from ...i18n import tr
from ...ui.icons import icon, icon_picker_combo
from .node_property_actions_mixin import _StepConfigActionsMixin
@@ -65,6 +65,8 @@ class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
self._step: Optional[Step] = None
self._node_id = ""
self._loading = False
self._ctx_available = ctx is not None
self._locked = False
self.setWidgetResizable(True)
host = QWidget()
self.setWidget(host)
@@ -281,6 +283,27 @@ class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
self.sub_list.addItem(sub.agent)
self._loading = False
def set_locked(self, locked: bool) -> None:
"""Khoá/mở khoá các trường chỉnh sửa theo trạng thái chạy của bước.
Bước đang chạy hoặc đã chạy xong thì khoá lại — tránh sửa nhầm cấu
hình trong lúc đang xem kết quả của chính lần chạy đó (sửa xong
không rõ là áp dụng cho lần chạy đã xong hay lần chạy tiếp theo).
Nút Chạy/Chạy từ đây/Xoá bước vẫn hoạt động bình thường khi khoá —
chỉ ô nhập liệu bị khoá, không phải cả panel.
"""
self._locked = locked
editable = not locked
for w in (self.label_edit, self.role_edit, self.icon_edit,
self.instructions_edit, self.context_edit,
self.model_combo, self.perm_combo, self.verify_chk,
self.rounds_spin, self.skills_list,
self.attach_add_btn, self.attach_del_btn,
self.sub_add_btn, self.sub_del_btn, self.sub_list):
w.setEnabled(editable)
self.gen_btn.setEnabled(editable and self._ctx_available)
self.load_models_btn.setEnabled(editable and self._ctx_available)
def clear_step(self) -> None:
"""Xoá bảng khi không có bước nào được chọn."""
self._step = None