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>
113 lines
4.3 KiB
Python
113 lines
4.3 KiB
Python
"""Catch controls orphaned by a neighbouring container being removed.
|
||
|
||
The audit page defaults every control to "giữ nguyên tại chỗ" and lists only the
|
||
ones that move. That default is unsafe when the thing a control sits *with* is
|
||
removed — then "unchanged" is impossible and the control has quietly lost its
|
||
home. This is how the Co4E "+ new workflow" button vanished from the proposal:
|
||
it lives in the same layout row as the flow tab strip, and the strip was proposed
|
||
for removal.
|
||
|
||
Note the relationship is SIBLING, not parent/child: `flow_row` holds both the
|
||
scroller (wrapping `flow_bar`) and `flow_add_btn`. An earlier version of this
|
||
check looked only for `container.addWidget(child)` and therefore found nothing —
|
||
it passed while the bug was live. Verify any change here with --selftest.
|
||
|
||
Run: python tools/check_orphans.py [--selftest]
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import ast
|
||
import io
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
REPO = Path(__file__).resolve().parent.parent
|
||
sys.path.insert(0, str(REPO / "tools"))
|
||
|
||
# A MOVES note containing one of these means the thing is going away, so anything
|
||
# that only existed alongside it needs a new home.
|
||
REMOVAL_WORDS = ("bỏ;", "bỏ ", "gộp", "thay thế")
|
||
|
||
|
||
def layout_map(path: Path) -> tuple[dict[str, list[str]], dict[str, str]]:
|
||
"""(layout var -> widget vars added to it, wrapper var -> widget it wraps)."""
|
||
members: dict[str, list[str]] = {}
|
||
alias: dict[str, str] = {}
|
||
tree = ast.parse(io.open(path, encoding="utf-8").read(), filename=str(path))
|
||
for node in ast.walk(tree):
|
||
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute):
|
||
continue
|
||
try:
|
||
owner = ast.unparse(node.func.value)
|
||
args = [ast.unparse(a) for a in node.args]
|
||
except Exception: # noqa: BLE001
|
||
continue
|
||
if not args:
|
||
continue
|
||
if node.func.attr in ("addWidget", "addLayout"):
|
||
members.setdefault(owner, []).append(args[0])
|
||
elif node.func.attr == "setWidget":
|
||
# QScrollArea(inner): the scroller stands in for what it holds.
|
||
alias[owner] = args[0]
|
||
return members, alias
|
||
|
||
|
||
def main(argv: list[str]) -> int:
|
||
import build_audit_page as B
|
||
|
||
selftest = "--selftest" in argv
|
||
moves = dict(B.MOVES)
|
||
if selftest:
|
||
# Re-create the original bug and prove the check reports it.
|
||
moves.pop("self.flow_add_btn", None)
|
||
|
||
removed = {k for k, v in moves.items()
|
||
if any(w in v.lower() for w in REMOVAL_WORDS)}
|
||
ctl = json.loads((REPO / "docs" / "screens" / "controls.json")
|
||
.read_text(encoding="utf-8"))
|
||
|
||
problems: list[tuple[str, str, str, str]] = []
|
||
n_sib = 0
|
||
for rec in ctl:
|
||
path = REPO / rec["file"]
|
||
if not path.exists():
|
||
continue
|
||
members, alias = layout_map(path)
|
||
labels = {c["var"]: (c.get("label_vi") or c.get("label") or "?")
|
||
for c in rec["controls"]}
|
||
for layout, kids in members.items():
|
||
# Resolve wrappers so a scroller counts as the widget it holds.
|
||
resolved = {k: alias.get(k, k) for k in kids}
|
||
gone = [k for k, r in resolved.items() if r in removed]
|
||
if not gone:
|
||
continue
|
||
for kid in kids:
|
||
if resolved[kid] in removed or kid not in labels:
|
||
continue
|
||
n_sib += 1
|
||
if kid not in moves:
|
||
problems.append((rec["file"], kid, labels[kid],
|
||
f"cung hang voi {resolved[gone[0]]}"))
|
||
|
||
print(f"control nam canh mot thanh phan bi bo : {n_sib}")
|
||
print(f"thanh phan bi bo trong MOVES : {len(removed)}"
|
||
f" {sorted(removed) if removed else ''}")
|
||
print()
|
||
if problems:
|
||
print("*** CONTROL MO COI ***")
|
||
for f, var, label, why in problems:
|
||
print(f" {f}: {var} ({label}) — {why}")
|
||
print()
|
||
print(f"KET QUA: {len(problems)} control mat cho, can khai bao trong MOVES")
|
||
return 0 if selftest else 1
|
||
if selftest:
|
||
print("KET QUA SELFTEST: *** THAT BAI — phep kiem KHONG bat duoc loi da biet ***")
|
||
return 1
|
||
print("KET QUA: khong co control nao bi mo coi")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main(sys.argv[1:]))
|