Implements the redesign from docs/ui-audit.html. Rearrangement only — no
feature was removed; every control that moved kept its handler, and the
gates that used to hide things now grey them out instead.
Navigation
* The rail is one flat list: the five Workspace sub-views sit at the top
level instead of behind an accordion, with Dashboard/Monitoring pinned
at the foot and Settings below them.
* Cowork and GraphRAG stay listed and greyed while no project is
selected, rather than vanishing and resizing the menu under the user.
* Monitoring keeps its eight sub-views in its own tab strip (unhidden)
instead of doubling the rail's length.
* _goto now moves the highlight itself, fixing a long-standing bug where
programmatic navigation left the rail pointing at the previous screen.
* Rail header gained the project picker and "New chat"; RECENTS lists the
active project's threads. Both are second views of existing state — the
Cowork toolbar button and the full History panel are untouched.
* Provider / language / theme moved from the top bar to an account row at
the foot of the rail (same widgets, same signals).
Screens
* Co4E: the flow tab strip is gone (per the design); Flow Status became a
toolbar toggle with its own way back, and the three icon-only tabs became
four labelled, foldable sections in one column. One flow open at a time
is the one capability this costs; background runs are unaffected.
* Dashboard: header split into two rows; cost promoted to a hero card.
* Monitoring Overview: one scrolling column of titled sections; the model
price table got its own full-width section instead of sharing a row with
the CPU meters.
* Settings and Task editor gained a section index down the left.
* Help dock: 84x64 launcher + chevron became one 26px dot that expands to
a labelled pill on hover; "hide to the edge" moved into the panel's menu.
Layout
* The window's minimum width dropped from 1453px to 768px. The main cause
was a QTabWidget taking its minimum from the widest page even when that
page is hidden, so Co4E was forcing Project and Cowork wide.
* Secondary panes fold themselves on a narrow window and restore when it
grows, never overriding a fold the user made.
* The long dialogs no longer scroll sideways at any font size.
Verification: tools/check_*.py build a real MainWindow offscreen against a
copy of ~/.cowork_local with the schedulers no-oped. check_design_parity.py
reads its checklist straight from the audit page's own proposals.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
245 lines
11 KiB
Python
245 lines
11 KiB
Python
"""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())
|