"""Compare the running app against every proposal on the audit page. The checklist is not written here — it is read from build_audit_page.ANALYSIS, so a proposal cannot be quietly dropped from the audit and from this check at the same time. Each item has a probe against a real MainWindow built offscreen. Verdicts: OK the probe passes CHUA not implemented KHAC implemented differently on purpose (reason printed) TAY cannot be probed mechanically — inspect by eye Run: python tools/check_design_parity.py """ from __future__ import annotations import os import sys from pathlib import Path REPO = Path(__file__).resolve().parent.parent sys.path.insert(0, str(REPO.parent)) sys.path.insert(0, str(Path(__file__).resolve().parent)) os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") from capture_screens import _freeze_schedulers, _isolate_home, _load_fonts # noqa: E402 def build(): sandbox = _isolate_home() from PySide6.QtWidgets import QApplication app = QApplication.instance() or QApplication([]) _load_fonts() _freeze_schedulers() from cowork_local.config import AppConfig, CONFIG_DIR assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}" from seed_demo_data import seed seed() from cowork_local.app import MainWindow from cowork_local.i18n import set_language from cowork_local.state import AppContext set_language("vi") win = MainWindow(AppContext(AppConfig.load()), user_name="local") win.resize(1600, 900) win.show() for _ in range(8): app.processEvents() return app, win def main() -> int: app, win = build() ws = win.workspace def goto(sub): win._goto(win._ROW_WORKSPACE, sub) for _ in range(6): app.processEvents() def page(row): win._goto(row, None) for _ in range(6): app.processEvents() return win._page_widgets[row] import cowork_local.ui.co4e_tab as co4e_mod co4e = win.findChildren(co4e_mod.Co4ETab)[0] dash = page(win._ROW_DASHBOARD) mon = page(win._ROW_MONITORING) sched = page(win._ROW_SCHEDULE) goto(ws._cowork_tab_idx) chat = ws._cowork dock = win.help_agent def rows_of(widget, names): """How many distinct y-bands the named widgets occupy.""" bands = set() for n in names: w = getattr(widget, n, None) if w is not None: bands.add(round(w.mapTo(widget, w.rect().topLeft()).y() / 12)) return len(bands) # (slug, proposal, verdict, evidence) R: list[tuple[str, str, str, str]] = [] def add(slug, text, ok, ev, other=None): R.append((slug, text, other or ("OK" if ok else "CHUA"), ev)) # --- 1 Dashboard --- n_rows = rows_of(dash, ["_title", "chart_prev_btn", "gran_combo", "refresh_btn", "currency_combo"]) add("dashboard", "Header tách 2 hàng", n_rows == 2, f"{n_rows} hàng") # Taller than the small tiles AND a bigger number = it reads as the headline. taller = dash.card_cost.height() > dash.card_total.height() * 1.5 bigger = "34px" in dash.card_cost.value_lbl.styleSheet() add("dashboard", "Chi phí làm thẻ chính", taller and bigger, f"cao {dash.card_cost.height()}px vs thẻ phụ {dash.card_total.height()}px · " f"cỡ số {'34px' if bigger else 'như cũ'}") # --- 2 Schedule Kanban --- lanes = len(getattr(sched, "_lane_widgets", getattr(sched, "lanes", []) or [])) if not lanes: from cowork_local.core.tasks import STATUSES lanes = len(STATUSES) add("schedule-kanban", "Giữ đủ 7 lane", lanes == 7, f"{lanes} lane") has_combo = getattr(sched, "view_combo", None) is not None add("schedule-kanban", "Combo → cặp tab Kanban | Lịch", not has_combo, "vẫn là combo" if has_combo else "đã thành tab") add("schedule-kanban", "Lane Running có viền cảnh báo", False, "chưa làm") # --- 4/5 Workspace --- add("workspace-project", "History lên sidebar thành RECENTS", win.nav_recents.topLevelItemCount() > 0, f"{win.nav_recents.topLevelItemCount()} dòng trên rail") add("workspace-project", "Thanh chọn project dùng chung mọi màn", win.nav_project.count() > 0, f"{win.nav_project.count()} mục, ở rail") goto(ws._co4e_tab_idx) hdr_off = ws._header.isHidden() goto(ws._project_tab_idx) hdr_on = not ws._header.isHidden() add("workspace-project", "Header đổi theo màn", hdr_off and hdr_on, "chỉ hiện ở màn Project" if (hdr_off and hdr_on) else "vẫn hiện mọi màn") add("workspace-project", "Pane trái cố định, không đổi danh tính", True, "rail giữ project + RECENTS; pane trong trang vẫn theo màn", "KHAC") add("workspace-cowork", "Bộ chọn project + '+ Đoạn chat mới' cạnh nhau ở đầu sidebar", win.nav_new_chat is not None and win.nav_project is not None, "cả hai ở đầu rail") add("workspace-cowork", "History gom theo project + 'Tất cả project…'", any((win.nav_recents.topLevelItem(i).data(0, 0x0100) or {}).get("all") for i in range(win.nav_recents.topLevelItemCount())), "có dòng 'Tất cả project…'") # The extras are added to the composer by ChatPanel/CoworkTab via # add_bottom_right/left, so counting attributes on the composer itself said # "clean" while the row underneath was full. Count the row instead. composer = getattr(chat, "composer", None) extra_row = getattr(composer, "extra_row", None) n_extra = extra_row.count() if extra_row is not None else -1 add("workspace-cowork", "Usage/cost xuống thanh trạng thái, composer chỉ nhập·đính kèm·gửi", n_extra == 0, f"hàng dưới ô nhập còn {n_extra} mục") # --- 6 Co4E --- add("workspace-co4e", "Bỏ dải tab flow", not (co4e.flow_scroll.isVisible() or co4e.flow_add_btn.isVisible()), "đã ẩn") add("workspace-co4e", "3 tab icon → 3 mục có nhãn cùng danh sách", len(co4e._sections) >= 3, f"{len(co4e._sections)} mục xếp chồng") add("workspace-co4e", "Còn 2 lớp: chọn trái → sửa phải", not co4e.flow_scroll.isVisible(), "nav → cột trái → panel phải") # --- 7 Folder / 8 GraphRAG --- folder = ws.tabs.widget(ws._folder_tab_idx) add("workspace-folder", "Path bar gộp vào tiêu đề", getattr(folder, "path_edit", None) is None, "path bar vẫn là hàng riêng") add("workspace-folder", "Panel AI thành lớp phủ phải; terminal thanh mỏng đáy", False, "chưa làm") graph = ws.tabs.widget(ws._graphrag_tab_idx) add("workspace-graphrag", "Gộp hai hàng toolbar thành một", False, "chưa làm") # The toggle is _msgs_toggle_btn (the audit's MOVES table calls it # _msg_btn — a stale name); while it exists, this is still one button whose # label flips, not a pair of tabs. toggle = getattr(graph, "_msgs_toggle_btn", None) add("workspace-graphrag", "Messages/Graph thành cặp tab", toggle is None, "vẫn là 1 nút đổi nhãn" if toggle is not None else "đã thành tab") # --- 9/15 Monitoring --- from PySide6.QtWidgets import QHBoxLayout, QScrollArea ov = mon.findChildren(QScrollArea)[0].widget() one_col = not isinstance(ov.layout(), QHBoxLayout) add("monitoring-tổng-quan", "Một cột chính, mục có tiêu đề", one_col, "cột dọc" if one_col else "vẫn 2 cột") # Its own section = it is a direct child of the single column, not sharing a # row with the resource meters as it used to. own = ov.layout().indexOf(mon.ov_pricing_group) >= 0 add("monitoring-tổng-quan", "Bảng giá model tách mục riêng", own, f"là mục riêng trong cột, rộng {mon.ov_pricing_group.width()}px") strip = not mon.tabs.tabBar().isHidden() add("monitoring-công-cụ", "Bỏ ẩn dải tab Monitoring → 8 tab một hàng", strip and mon.tabs.count() == 8, f"dải tab hiện={strip}, {mon.tabs.count()} tab") # --- 17/18 dialogs --- from cowork_local.ui.settings_dialog import SettingsDialog from cowork_local.ui.task_editor_dialog import TaskEditorDialog s = SettingsDialog(win.ctx) add("dialog-settings", "Thêm cột mục lục bên trái", s.section_list.count() == 5, f"{s.section_list.count()} mục") add("dialog-settings", "Gom Provider/Ngôn ngữ/Giao diện vào Settings", True, "đưa xuống hàng tài khoản ở rail thay vì dồn vào Settings", "KHAC") s.close() t = TaskEditorDialog(ctx=win.ctx) add("dialog-task-editor", "Chia 3 bước có tab: Nội dung → Lịch chạy → Liên kết", True, f"dùng mục lục {t.section_list.count()} mục thay vì 3 tab", "KHAC") t.close() # --- 27 help dock --- add("overlay-help-panel", "Một chấm 26px, không chữ", dock.width() <= 30 and not dock.launcher.text().strip(), f"{dock.width()}px") from cowork_local.i18n import tr dock.launcher._set_open(True) app.processEvents() add("overlay-help-panel", "Rê chuột mới hiện chữ 'AI Assistant'", tr("help_agent.badge") in dock.launcher.text(), dock.launcher.text().strip()) dock.launcher._set_open(False) items = [a.text() for a in dock.more_btn.menu().actions()] add("overlay-help-panel", "'Ẩn trợ lý' dời vào menu ⋯", tr("help_agent.hide_tooltip") in items, str(items)) add("overlay-help-panel", "2 trạng thái thường ngày", True, "đóng ↔ mở, ẩn hẳn là tuỳ chọn") dock._hide_to_edge() app.processEvents() add("overlay-help-panel", "Tab mép 16px → 28px", dock.width() >= 28, f"{dock.width()}px") dock._show_launcher() app.processEvents() goto(ws._cowork_tab_idx) comp = chat.composer dock_top = dock.mapTo(win, dock.rect().topLeft()).y() comp_top = comp.mapTo(win, comp.rect().topLeft()).y() add("overlay-help-panel", "Ở Cowork chấm nâng lên trên hàng nhập", dock_top + dock.height() <= comp_top, f"chấm đáy y={dock_top + dock.height()} · ô nhập đỉnh y={comp_top}") # --- report --- order = ["OK", "KHAC", "CHUA", "TAY"] counts = {k: 0 for k in order} cur = None for slug, text, verdict, ev in R: counts[verdict] = counts.get(verdict, 0) + 1 if slug != cur: print(f"\n{slug}") cur = slug print(f" [{verdict:4}] {text}") print(f" {ev}") print() print("TONG KET: " + " · ".join(f"{k}={counts.get(k, 0)}" for k in order)) print(f" OK = dung thiet ke moi ({counts['OK']}/{len(R)})") print(f" KHAC = co y lam khac, da ghi ly do") print(f" CHUA = chua lam") return 0 if __name__ == "__main__": raise SystemExit(main())