fix(tools): 5 checker UI hỏng sau đợt tách widget R08
`tools/` không đổi một byte nào giữa hai bản, nhưng 5 checker vẫn chết vì chúng tìm control bằng `getattr(root, "ten")` trên đúng widget cũ — mà R08 đã dời control xuống widget con. Thêm ba helper dùng chung vào `capture_screens.py`: * `_own_member` — tên do app khai trên widget, không phải thừa kế từ Qt * `owner_of` — widget thật sự đang giữ tên đó, duyệt theo bề rộng * `control` — lấy control dù nó nằm ở cấp nào `check_controls_alive` từ "MẤT 24 control" về 0, kèm liệt kê 22 control đã đổi chỗ và 2 cái đổi tên. `check_probes_bite` từ 1/4 lên 6/6 phép cấy lỗi đều bị bắt — phép cấy thứ hai trỏ vào `ui/schedule_task_tab.py` đã bị xoá, nay trỏ vào `presentation/scheduling/kanban_board_widget.py`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,63 @@ OUT_DIR = REPO / "docs" / "screens"
|
|||||||
THEMES = ("dark", "light")
|
THEMES = ("dark", "light")
|
||||||
|
|
||||||
|
|
||||||
|
def _own_member(widget, name: str) -> bool:
|
||||||
|
"""True when `name` is declared by the app on `widget`, not inherited from Qt.
|
||||||
|
|
||||||
|
Instance attributes live in ``vars(widget)``; methods live on the class, so
|
||||||
|
both are checked. Only classes defined inside ``cowork_local`` count, so a
|
||||||
|
Qt base class that happens to use the same name can never be mistaken for
|
||||||
|
the app's own control.
|
||||||
|
"""
|
||||||
|
if name in vars(widget):
|
||||||
|
return True
|
||||||
|
for base in type(widget).__mro__:
|
||||||
|
if getattr(base, "__module__", "").startswith("cowork_local") and name in vars(base):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def owner_of(root, name: str):
|
||||||
|
"""The widget in `root`'s subtree that actually holds `name` today.
|
||||||
|
|
||||||
|
EPIC R08 split every screen's god-widget into child widgets, so a control
|
||||||
|
that used to be ``tab.gran_combo`` now lives at ``tab.chart.gran_combo``,
|
||||||
|
and ``ScheduleTaskTab.columns`` moved to ``ScheduleTaskTab.kanban.columns``.
|
||||||
|
Checkers ask for a control by name and get back whichever widget owns it
|
||||||
|
today, so a further split does not break them again — while a control that
|
||||||
|
is genuinely gone still returns ``None`` and is still reported as a loss.
|
||||||
|
|
||||||
|
Breadth-first, so the shallowest owner wins if a name appears twice.
|
||||||
|
"""
|
||||||
|
from PySide6.QtWidgets import QWidget
|
||||||
|
|
||||||
|
seen: set[int] = set()
|
||||||
|
queue = [root]
|
||||||
|
while queue:
|
||||||
|
w = queue.pop(0)
|
||||||
|
if id(w) in seen:
|
||||||
|
continue
|
||||||
|
seen.add(id(w))
|
||||||
|
if _own_member(w, name):
|
||||||
|
return w
|
||||||
|
queue.extend(c for c in w.children() if isinstance(c, QWidget))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def control(root, name: str, default=None):
|
||||||
|
"""The control named `name` anywhere in `root`'s subtree — see `owner_of`.
|
||||||
|
|
||||||
|
Falls back to a plain ``getattr`` on `root` so a screen that already bridges
|
||||||
|
its old attribute names itself keeps working: ``MonitoringTab.__getattr__``
|
||||||
|
maps ``ov_*`` onto the extracted tabs, and a name served that way is on no
|
||||||
|
widget's ``__dict__`` for `owner_of` to find.
|
||||||
|
"""
|
||||||
|
holder = owner_of(root, name)
|
||||||
|
if holder is not None:
|
||||||
|
return getattr(holder, name, default)
|
||||||
|
return getattr(root, name, default)
|
||||||
|
|
||||||
|
|
||||||
def _isolate_home() -> Path:
|
def _isolate_home() -> Path:
|
||||||
"""Copy the real config dir into a temp HOME and repoint the env at it."""
|
"""Copy the real config dir into a temp HOME and repoint the env at it."""
|
||||||
real = Path.home() / ".cowork_local"
|
real = Path.home() / ".cowork_local"
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ sys.path.insert(0, str(REPO.parent))
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from capture_screens import _apply_theme, _freeze_schedulers, _isolate_home, _load_fonts # noqa: E402
|
from capture_screens import ( # noqa: E402
|
||||||
|
_apply_theme, _freeze_schedulers, _isolate_home, _load_fonts, owner_of,
|
||||||
|
)
|
||||||
|
|
||||||
# controls.json lists every control in a FILE, and several files hold more than
|
# controls.json lists every control in a FILE, and several files hold more than
|
||||||
# one class (schedule_task_tab.py alone has the tab plus three dialogs). Only
|
# one class (schedule_task_tab.py alone has the tab plus three dialogs). Only
|
||||||
@@ -82,6 +84,11 @@ MOVED = {
|
|||||||
},
|
},
|
||||||
"ui\\structure_graph_view.py": {
|
"ui\\structure_graph_view.py": {
|
||||||
"self._msgs_toggle_btn": "→ cặp tab Đồ thị | Tin nhắn (view_tabs)",
|
"self._msgs_toggle_btn": "→ cặp tab Đồ thị | Tin nhắn (view_tabs)",
|
||||||
|
# R08-T14 split the screen and renamed these two on the way out. Both
|
||||||
|
# are still on screen, under a new owner and a new name, so they are
|
||||||
|
# deliberate moves rather than losses.
|
||||||
|
"self._ag_collapse": "→ GraphQaWidget._collapse_btn (nút thu gọn bảng Agent)",
|
||||||
|
"self._msgs_view": "→ GraphMessagesView.widget (cây Tin nhắn theo ngày)",
|
||||||
},
|
},
|
||||||
"app.py": {
|
"app.py": {
|
||||||
"self.settings_btn": "→ nút Cài đặt ở đáy rail (_nav_settings_btn)",
|
"self.settings_btn": "→ nút Cài đặt ở đáy rail (_nav_settings_btn)",
|
||||||
@@ -163,8 +170,9 @@ def main() -> int:
|
|||||||
.read_text(encoding="utf-8"))
|
.read_text(encoding="utf-8"))
|
||||||
own = owners(win)
|
own = owners(win)
|
||||||
|
|
||||||
alive = dead = moved = skipped = other_class = 0
|
alive = dead = moved = skipped = other_class = relocated = 0
|
||||||
losses: list[tuple[str, str, str]] = []
|
losses: list[tuple[str, str, str]] = []
|
||||||
|
moves: list[tuple[str, str, str]] = []
|
||||||
for rec in index:
|
for rec in index:
|
||||||
holder = own.get(rec["file"])
|
holder = own.get(rec["file"])
|
||||||
if holder is None:
|
if holder is None:
|
||||||
@@ -187,14 +195,28 @@ def main() -> int:
|
|||||||
elif var in MOVED.get(rec["file"], {}):
|
elif var in MOVED.get(rec["file"], {}):
|
||||||
moved += 1
|
moved += 1
|
||||||
else:
|
else:
|
||||||
dead += 1
|
# EPIC R08 extracted sub-widgets out of every screen, so a
|
||||||
losses.append((rec["file"], var,
|
# control can still be on screen while no longer being a direct
|
||||||
c.get("label_vi") or c.get("label") or "?"))
|
# attribute of the screen's own widget (FolderTab.mode_btn ->
|
||||||
|
# FolderTab.preview.mode_btn). Searching the subtree keeps the
|
||||||
|
# subtraction test honest: it still fails on a control that is
|
||||||
|
# genuinely gone, but a relocation now reads as a relocation.
|
||||||
|
sub = owner_of(holder, name)
|
||||||
|
if sub is not None:
|
||||||
|
relocated += 1
|
||||||
|
moves.append((rec["file"], var, type(sub).__name__))
|
||||||
|
else:
|
||||||
|
dead += 1
|
||||||
|
losses.append((rec["file"], var,
|
||||||
|
c.get("label_vi") or c.get("label") or "?"))
|
||||||
|
|
||||||
print(f"control con song : {alive}")
|
print(f"control con song : {alive}")
|
||||||
print(f"co y doi cho : {moved}")
|
print(f"co y doi cho : {moved}")
|
||||||
for f, v, w in [(f, v, MOVED[f][v]) for f in MOVED for v in MOVED[f]]:
|
for f, v, w in [(f, v, MOVED[f][v]) for f in MOVED for v in MOVED[f]]:
|
||||||
print(f" {v:26} {w}")
|
print(f" {v:26} {w}")
|
||||||
|
print(f"doi cho khi tach : {relocated} (con tren man, nam trong widget con)")
|
||||||
|
for f, var, own in sorted(moves):
|
||||||
|
print(f" {var:26} -> {own}.{var.split('.', 1)[1]}")
|
||||||
print(f"thuoc class khac : {other_class} (hop thoai dinh nghia cung file)")
|
print(f"thuoc class khac : {other_class} (hop thoai dinh nghia cung file)")
|
||||||
print(f"khong kiem duoc : {skipped} (dialog dung rieng, bien cuc bo)")
|
print(f"khong kiem duoc : {skipped} (dialog dung rieng, bien cuc bo)")
|
||||||
print(f"MAT : {dead}")
|
print(f"MAT : {dead}")
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ sys.path.insert(0, str(REPO.parent))
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from capture_screens import _apply_theme, _isolate_home, _load_fonts # noqa: E402
|
from capture_screens import _apply_theme, _isolate_home, _load_fonts, control # noqa: E402
|
||||||
|
|
||||||
HEADER = ["_title", "chart_prev_btn", "_chart_period_lbl", "chart_next_btn",
|
HEADER = ["_title", "chart_prev_btn", "_chart_period_lbl", "chart_next_btn",
|
||||||
"gran_combo", "metric_combo", "currency_lbl", "currency_combo",
|
"gran_combo", "metric_combo", "currency_lbl", "currency_combo",
|
||||||
@@ -42,7 +42,11 @@ def main() -> int:
|
|||||||
|
|
||||||
from cowork_local.i18n import set_language
|
from cowork_local.i18n import set_language
|
||||||
from cowork_local.state import AppContext
|
from cowork_local.state import AppContext
|
||||||
from cowork_local.ui.dashboard_tab import DashboardTab
|
# R08-T13 moved the screen out of ui/ into presentation/dashboard/ and
|
||||||
|
# split its header controls across UsageChartWidget / HabitsWidget, so
|
||||||
|
# every control below is looked up through `control()` rather than as a
|
||||||
|
# direct attribute of the tab.
|
||||||
|
from cowork_local.presentation.dashboard.dashboard_tab import DashboardTab
|
||||||
|
|
||||||
set_language("vi")
|
set_language("vi")
|
||||||
tab = DashboardTab(AppContext(AppConfig.load()))
|
tab = DashboardTab(AppContext(AppConfig.load()))
|
||||||
@@ -53,7 +57,7 @@ def main() -> int:
|
|||||||
app.processEvents()
|
app.processEvents()
|
||||||
|
|
||||||
fails: list[str] = []
|
fails: list[str] = []
|
||||||
missing = [n for n in HEADER if getattr(tab, n, None) is None]
|
missing = [n for n in HEADER if control(tab, n) is None]
|
||||||
print(f"control header con nguyen: {len(HEADER) - len(missing)}/{len(HEADER)}")
|
print(f"control header con nguyen: {len(HEADER) - len(missing)}/{len(HEADER)}")
|
||||||
if missing:
|
if missing:
|
||||||
fails.append(f"mat control: {missing}")
|
fails.append(f"mat control: {missing}")
|
||||||
@@ -61,7 +65,7 @@ def main() -> int:
|
|||||||
# Two rows: everything in the header must sit at one of exactly two y bands.
|
# Two rows: everything in the header must sit at one of exactly two y bands.
|
||||||
tops = {}
|
tops = {}
|
||||||
for n in HEADER:
|
for n in HEADER:
|
||||||
w = getattr(tab, n)
|
w = control(tab, n)
|
||||||
tops.setdefault(round(w.mapTo(tab, w.rect().topLeft()).y() / 10), []).append(n)
|
tops.setdefault(round(w.mapTo(tab, w.rect().topLeft()).y() / 10), []).append(n)
|
||||||
print(f"so hang cua header : {len(tops)}")
|
print(f"so hang cua header : {len(tops)}")
|
||||||
for band, names in sorted(tops.items()):
|
for band, names in sorted(tops.items()):
|
||||||
@@ -70,14 +74,15 @@ def main() -> int:
|
|||||||
fails.append(f"header co {len(tops)} hang, cho 2")
|
fails.append(f"header co {len(tops)} hang, cho 2")
|
||||||
|
|
||||||
# Still wired: changing the metric must not throw and must stick.
|
# Still wired: changing the metric must not throw and must stick.
|
||||||
before = tab.metric_combo.currentData()
|
metric = control(tab, "metric_combo")
|
||||||
tab.metric_combo.setCurrentIndex(1 - tab.metric_combo.currentIndex())
|
before = metric.currentData()
|
||||||
|
metric.setCurrentIndex(1 - metric.currentIndex())
|
||||||
app.processEvents()
|
app.processEvents()
|
||||||
after = tab.metric_combo.currentData()
|
after = metric.currentData()
|
||||||
print(f"doi chi so bieu do : {before} -> {after}")
|
print(f"doi chi so bieu do : {before} -> {after}")
|
||||||
if after == before:
|
if after == before:
|
||||||
fails.append("combo chi so khong doi duoc")
|
fails.append("combo chi so khong doi duoc")
|
||||||
tab.refresh_btn.click()
|
control(tab, "refresh_btn").click()
|
||||||
app.processEvents()
|
app.processEvents()
|
||||||
print("bam Lam moi : khong loi")
|
print("bam Lam moi : khong loi")
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,16 @@ sys.path.insert(0, str(REPO.parent))
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from capture_screens import _apply_theme, _freeze_schedulers, _isolate_home, _load_fonts # noqa: E402
|
from capture_screens import ( # noqa: E402
|
||||||
|
_apply_theme, _freeze_schedulers, _isolate_home, _load_fonts, control,
|
||||||
|
)
|
||||||
|
|
||||||
|
# EPIC R08 split every screen's god-widget into child widgets, so controls
|
||||||
|
# this file used to read straight off the screen object now live one level
|
||||||
|
# down (DashboardTab.chart.gran_combo, ScheduleTaskTab.kanban.columns, the
|
||||||
|
# Monitoring Overview sections, the Settings sub-pages...). `control()`
|
||||||
|
# looks a name up anywhere in the screen's subtree, so this checker keeps
|
||||||
|
# measuring the real control and still returns None when one is truly gone.
|
||||||
|
|
||||||
|
|
||||||
def page_proposals():
|
def page_proposals():
|
||||||
@@ -107,7 +116,7 @@ def main() -> int:
|
|||||||
"""How many distinct y-bands the named widgets occupy."""
|
"""How many distinct y-bands the named widgets occupy."""
|
||||||
bands = set()
|
bands = set()
|
||||||
for n in names:
|
for n in names:
|
||||||
w = getattr(widget, n, None)
|
w = control(widget, n)
|
||||||
if w is not None:
|
if w is not None:
|
||||||
bands.add(round(w.mapTo(widget, w.rect().topLeft()).y() / 12))
|
bands.add(round(w.mapTo(widget, w.rect().topLeft()).y() / 12))
|
||||||
return len(bands)
|
return len(bands)
|
||||||
@@ -123,29 +132,31 @@ def main() -> int:
|
|||||||
"currency_combo"])
|
"currency_combo"])
|
||||||
add("dashboard", "Header tách 2 hàng", n_rows == 2, f"{n_rows} hàng")
|
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 than the small tiles AND a bigger number = it reads as the headline.
|
||||||
taller = dash.card_cost.height() > dash.card_total.height() * 1.5
|
card_cost = control(dash, "card_cost")
|
||||||
bigger = "34px" in dash.card_cost.value_lbl.styleSheet()
|
card_total = control(dash, "card_total")
|
||||||
|
taller = card_cost.height() > card_total.height() * 1.5
|
||||||
|
bigger = "34px" in card_cost.value_lbl.styleSheet()
|
||||||
add("dashboard", "Chi phí làm thẻ chính", taller and bigger,
|
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"cao {card_cost.height()}px vs thẻ phụ {card_total.height()}px · "
|
||||||
f"cỡ số {'34px' if bigger else 'như cũ'}")
|
f"cỡ số {'34px' if bigger else 'như cũ'}")
|
||||||
|
|
||||||
# --- 2 Schedule Kanban ---
|
# --- 2 Schedule Kanban ---
|
||||||
lanes = len(getattr(sched, "_lane_widgets", getattr(sched, "lanes", []) or []))
|
lanes = len(control(sched, "columns") or {})
|
||||||
if not lanes:
|
if not lanes:
|
||||||
from cowork_local.core.tasks import STATUSES
|
from cowork_local.core.tasks import STATUSES
|
||||||
lanes = len(STATUSES)
|
lanes = len(STATUSES)
|
||||||
add("schedule-kanban", "Giữ đủ 7 lane", lanes == 7, f"{lanes} lane")
|
add("schedule-kanban", "Giữ đủ 7 lane", lanes == 7, f"{lanes} lane")
|
||||||
has_combo = getattr(sched, "view_combo", None) is not None
|
has_combo = control(sched, "view_combo") is not None
|
||||||
add("schedule-kanban", "Combo → cặp tab Kanban | Lịch", not has_combo,
|
add("schedule-kanban", "Combo → cặp tab Kanban | Lịch", not has_combo,
|
||||||
"vẫn là combo" if has_combo else "đã thành tab")
|
"vẫn là combo" if has_combo else "đã thành tab")
|
||||||
# The lane is only outlined while it actually holds something — seed data
|
# The lane is only outlined while it actually holds something — seed data
|
||||||
# may leave it empty, so drop a card in and read the style back.
|
# may leave it empty, so drop a card in and read the style back.
|
||||||
run_col = sched.columns.get("running")
|
run_col = (control(sched, "columns") or {}).get("running")
|
||||||
styled = ""
|
styled = ""
|
||||||
if run_col is not None:
|
if run_col is not None:
|
||||||
from PySide6.QtWidgets import QListWidgetItem
|
from PySide6.QtWidgets import QListWidgetItem
|
||||||
run_col.addItem(QListWidgetItem("probe"))
|
run_col.addItem(QListWidgetItem("probe"))
|
||||||
sched.column_headers["running"].setStyleSheet("")
|
control(sched, "column_headers")["running"].setStyleSheet("")
|
||||||
sched.refresh()
|
sched.refresh()
|
||||||
app.processEvents()
|
app.processEvents()
|
||||||
styled = run_col.styleSheet()
|
styled = run_col.styleSheet()
|
||||||
@@ -184,13 +195,13 @@ def main() -> int:
|
|||||||
# status line under the typing box, not inside it. So the test is that the
|
# status line under the typing box, not inside it. So the test is that the
|
||||||
# TYPING row holds only input + attach/send/stop, and the rest sits in its
|
# TYPING row holds only input + attach/send/stop, and the rest sits in its
|
||||||
# own strip below. Demanding an empty strip would mean deleting features.
|
# own strip below. Demanding an empty strip would mean deleting features.
|
||||||
composer = getattr(chat, "composer", None)
|
composer = control(chat, "composer")
|
||||||
bar = getattr(composer, "extra_bar", None)
|
bar = control(composer, "extra_bar")
|
||||||
from PySide6.QtWidgets import QPlainTextEdit, QTextEdit
|
from PySide6.QtWidgets import QPlainTextEdit, QTextEdit
|
||||||
typing = composer.input
|
typing = composer.input
|
||||||
in_typing_row = typing.parentWidget() is composer
|
in_typing_row = typing.parentWidget() is composer
|
||||||
below = bar is not None and bar.objectName() == "composerStatus"
|
below = bar is not None and bar.objectName() == "composerStatus"
|
||||||
usage = getattr(chat, "_usage_total_lbl", None)
|
usage = control(chat, "_usage_total_lbl")
|
||||||
usage_in_bar = usage is not None and bar is not None and bar.isAncestorOf(usage)
|
usage_in_bar = usage is not None and bar is not None and bar.isAncestorOf(usage)
|
||||||
add("workspace-cowork", "Usage/cost thành dải trạng thái; vùng gõ chỉ nhập·đính kèm·gửi",
|
add("workspace-cowork", "Usage/cost thành dải trạng thái; vùng gõ chỉ nhập·đính kèm·gửi",
|
||||||
below and usage_in_bar,
|
below and usage_in_bar,
|
||||||
@@ -199,23 +210,25 @@ def main() -> int:
|
|||||||
|
|
||||||
# --- 6 Co4E ---
|
# --- 6 Co4E ---
|
||||||
add("workspace-co4e", "Bỏ dải tab flow",
|
add("workspace-co4e", "Bỏ dải tab flow",
|
||||||
not (co4e.flow_scroll.isVisible() or co4e.flow_add_btn.isVisible()), "đã ẩn")
|
not (control(co4e, "flow_scroll").isVisible()
|
||||||
|
or control(co4e, "flow_add_btn").isVisible()), "đã ẩn")
|
||||||
|
sections = control(co4e, "_sections") or []
|
||||||
add("workspace-co4e", "3 tab icon → 3 mục có nhãn cùng danh sách",
|
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")
|
len(sections) >= 3, f"{len(sections)} mục xếp chồng")
|
||||||
add("workspace-co4e", "Còn 2 lớp: chọn trái → sửa phải",
|
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")
|
not control(co4e, "flow_scroll").isVisible(), "nav → cột trái → panel phải")
|
||||||
|
|
||||||
# --- 7 Folder / 8 GraphRAG ---
|
# --- 7 Folder / 8 GraphRAG ---
|
||||||
folder = ws.tabs.widget(ws._folder_tab_idx)
|
folder = ws.tabs.widget(ws._folder_tab_idx)
|
||||||
title_lbl = getattr(folder, "path_lbl", None)
|
title_lbl = control(folder, "path_lbl")
|
||||||
add("workspace-folder", "Path bar gộp vào tiêu đề",
|
add("workspace-folder", "Path bar gộp vào tiêu đề",
|
||||||
title_lbl is not None and getattr(folder, "path_edit", None) is None,
|
title_lbl is not None and control(folder, "path_edit") is None,
|
||||||
f"tiêu đề = {title_lbl.text()[:40]!r}" if title_lbl is not None else "vẫn là ô nhập")
|
f"tiêu đề = {title_lbl.text()[:40]!r}" if title_lbl is not None else "vẫn là ô nhập")
|
||||||
# "Thin bar at the bottom" = the terminal is the last thing in the column
|
# "Thin bar at the bottom" = the terminal is the last thing in the column
|
||||||
# and starts collapsed; the AI panel is a hideable right-hand pane.
|
# and starts collapsed; the AI panel is a hideable right-hand pane.
|
||||||
# Geometry is meaningless for a page that has never been shown, so ask the
|
# Geometry is meaningless for a page that has never been shown, so ask the
|
||||||
# widgets what state they are in instead of how tall they currently are.
|
# widgets what state they are in instead of how tall they currently are.
|
||||||
term = getattr(folder, "terminal", None)
|
term = control(folder, "terminal")
|
||||||
lay = folder.layout()
|
lay = folder.layout()
|
||||||
last = lay.itemAt(lay.count() - 1).widget() if lay.count() else None
|
last = lay.itemAt(lay.count() - 1).widget() if lay.count() else None
|
||||||
collapsed = term is not None and term._body.isHidden()
|
collapsed = term is not None and term._body.isHidden()
|
||||||
@@ -227,13 +240,14 @@ def main() -> int:
|
|||||||
# One row = the path box and Export share a y-band.
|
# One row = the path box and Export share a y-band.
|
||||||
def band(w):
|
def band(w):
|
||||||
return round(w.mapTo(graph, w.rect().topLeft()).y() / 10)
|
return round(w.mapTo(graph, w.rect().topLeft()).y() / 10)
|
||||||
one_row = band(graph.path_edit) == band(graph._export_btn)
|
g_path, g_export = control(graph, "path_edit"), control(graph, "_export_btn")
|
||||||
|
one_row = band(g_path) == band(g_export)
|
||||||
add("workspace-graphrag", "Gộp hai hàng toolbar thành một", one_row,
|
add("workspace-graphrag", "Gộp hai hàng toolbar thành một", one_row,
|
||||||
f"path y≈{band(graph.path_edit) * 10} · Export y≈{band(graph._export_btn) * 10}")
|
f"path y≈{band(g_path) * 10} · Export y≈{band(g_export) * 10}")
|
||||||
# The toggle is _msgs_toggle_btn (the audit's MOVES table calls it
|
# 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
|
# _msg_btn — a stale name); while it exists, this is still one button whose
|
||||||
# label flips, not a pair of tabs.
|
# label flips, not a pair of tabs.
|
||||||
toggle = getattr(graph, "_msgs_toggle_btn", None)
|
toggle = control(graph, "_msgs_toggle_btn")
|
||||||
add("workspace-graphrag", "Messages/Graph thành cặp tab", toggle is 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")
|
"vẫn là 1 nút đổi nhãn" if toggle is not None else "đã thành tab")
|
||||||
|
|
||||||
@@ -248,7 +262,7 @@ def main() -> int:
|
|||||||
body = area.widget()
|
body = area.widget()
|
||||||
if body is None or body.layout() is None:
|
if body is None or body.layout() is None:
|
||||||
continue
|
continue
|
||||||
if body.layout().indexOf(mon.ov_usage_group) >= 0:
|
if body.layout().indexOf(control(mon, "ov_usage_group")) >= 0:
|
||||||
ov = body
|
ov = body
|
||||||
break
|
break
|
||||||
assert ov is not None, "khong tim thay cot Tong quan"
|
assert ov is not None, "khong tim thay cot Tong quan"
|
||||||
@@ -257,25 +271,29 @@ def main() -> int:
|
|||||||
"cột dọc" if one_col else "vẫn 2 cột")
|
"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
|
# Its own section = it is a direct child of the single column, not sharing a
|
||||||
# row with the resource meters as it used to.
|
# row with the resource meters as it used to.
|
||||||
own = ov.layout().indexOf(mon.ov_pricing_group) >= 0
|
pricing = control(mon, "ov_pricing_group")
|
||||||
|
own = ov.layout().indexOf(pricing) >= 0
|
||||||
add("monitoring-tổng-quan", "Bảng giá model tách mục riêng", own,
|
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")
|
f"là mục riêng trong cột, rộng {pricing.width()}px")
|
||||||
strip = not mon.tabs.tabBar().isHidden()
|
mon_tabs = control(mon, "tabs")
|
||||||
|
strip = not mon_tabs.tabBar().isHidden()
|
||||||
add("monitoring-công-cụ", "Bỏ ẩn dải tab Monitoring → 8 tab một hàng",
|
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")
|
strip and mon_tabs.count() == 8, f"dải tab hiện={strip}, {mon_tabs.count()} tab")
|
||||||
|
|
||||||
# --- 17/18 dialogs ---
|
# --- 17/18 dialogs ---
|
||||||
from cowork_local.ui.settings_dialog import SettingsDialog
|
from cowork_local.ui.settings_dialog import SettingsDialog
|
||||||
from cowork_local.ui.task_editor_dialog import TaskEditorDialog
|
from cowork_local.ui.task_editor_dialog import TaskEditorDialog
|
||||||
s = SettingsDialog(win.ctx)
|
s = SettingsDialog(win.ctx)
|
||||||
|
s_list = control(s, "section_list")
|
||||||
add("dialog-settings", "Thêm cột mục lục bên trái",
|
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")
|
s_list.count() == 5, f"{s_list.count()} mục")
|
||||||
# The page asks for Ngôn ngữ/Giao diện in the Chung group, with AI Provider
|
# The page asks for Ngôn ngữ/Giao diện in the Chung group, with AI Provider
|
||||||
# left as its own group — not everything merged together.
|
# left as its own group — not everything merged together.
|
||||||
from cowork_local.ui.widgets import SegmentedControl, ToggleSwitch
|
from cowork_local.ui.widgets import SegmentedControl, ToggleSwitch
|
||||||
in_general = s._general_box.isAncestorOf(s.language_combo) and \
|
gen_box = control(s, "_general_box")
|
||||||
s._general_box.isAncestorOf(s.theme_combo)
|
in_general = gen_box.isAncestorOf(control(s, "language_combo")) and \
|
||||||
prov_apart = not s._general_box.isAncestorOf(s.provider_combo)
|
gen_box.isAncestorOf(control(s, "theme_combo"))
|
||||||
|
prov_apart = not gen_box.isAncestorOf(control(s, "provider_combo"))
|
||||||
add("dialog-settings", "Gom Ngôn ngữ/Giao diện vào nhóm Chung; AI Provider vẫn riêng",
|
add("dialog-settings", "Gom Ngôn ngữ/Giao diện vào nhóm Chung; AI Provider vẫn riêng",
|
||||||
in_general and prov_apart,
|
in_general and prov_apart,
|
||||||
f"ngôn ngữ+giao diện trong Chung={in_general} · provider tách riêng={prov_apart}")
|
f"ngôn ngữ+giao diện trong Chung={in_general} · provider tách riêng={prov_apart}")
|
||||||
@@ -288,10 +306,11 @@ def main() -> int:
|
|||||||
f"{n_switch} toggle · {n_seg} segmented · {len(steppers)} stepper")
|
f"{n_switch} toggle · {n_seg} segmented · {len(steppers)} stepper")
|
||||||
s.close()
|
s.close()
|
||||||
t = TaskEditorDialog(ctx=win.ctx)
|
t = TaskEditorDialog(ctx=win.ctx)
|
||||||
rows = [t.section_list.item(i).text() for i in range(t.section_list.count())]
|
t_list, t_stack = control(t, "section_list"), control(t, "section_stack")
|
||||||
like_settings = (t.section_list.count() == 5
|
rows = [t_list.item(i).text() for i in range(t_list.count())]
|
||||||
and t.section_stack.count() == 5
|
like_settings = (t_list.count() == 5
|
||||||
and not hasattr(t, "step_tabs"))
|
and t_stack.count() == 5
|
||||||
|
and control(t, "step_tabs") is None)
|
||||||
add("dialog-task-editor",
|
add("dialog-task-editor",
|
||||||
"Danh sách trái + panel phải giống Cài đặt (không dùng tab), 5 mục = 5 group",
|
"Danh sách trái + panel phải giống Cài đặt (không dùng tab), 5 mục = 5 group",
|
||||||
like_settings, " · ".join(rows))
|
like_settings, " · ".join(rows))
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from capture_screens import ( # noqa: E402
|
from capture_screens import ( # noqa: E402
|
||||||
_apply_theme, _freeze_schedulers, _isolate_home, _load_fonts)
|
_apply_theme, _freeze_schedulers, _isolate_home, _load_fonts, owner_of)
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
@@ -50,6 +50,10 @@ def main() -> int:
|
|||||||
win.show()
|
win.show()
|
||||||
app.processEvents()
|
app.processEvents()
|
||||||
st, w = win.structure, win.workspace
|
st, w = win.structure, win.workspace
|
||||||
|
# R08-T14 split StructureGraphView: the browser view, the cached graph
|
||||||
|
# and the scan/render steps all moved onto GraphRenderer, while the shell
|
||||||
|
# only forwards the public methods. Probe the widget that owns them.
|
||||||
|
st = owner_of(st, "web") or st
|
||||||
fails: list[str] = []
|
fails: list[str] = []
|
||||||
|
|
||||||
# startup itself must not build any of it
|
# startup itself must not build any of it
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -21,7 +22,9 @@ sys.path.insert(0, str(REPO.parent))
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from capture_screens import _apply_theme, _freeze_schedulers, _isolate_home, _load_fonts # noqa: E402
|
from capture_screens import ( # noqa: E402
|
||||||
|
_apply_theme, _freeze_schedulers, _isolate_home, _load_fonts, control,
|
||||||
|
)
|
||||||
|
|
||||||
# The rail, top to bottom, as the audit page's rail() helper draws it.
|
# The rail, top to bottom, as the audit page's rail() helper draws it.
|
||||||
RAIL_ORDER = ["Project", "Cowork", "Co4E", "Thư mục", "GraphRAG", "Schedule Task"]
|
RAIL_ORDER = ["Project", "Cowork", "Co4E", "Thư mục", "GraphRAG", "Schedule Task"]
|
||||||
@@ -122,7 +125,9 @@ def main() -> int:
|
|||||||
app.processEvents()
|
app.processEvents()
|
||||||
sched = win._page_widgets[win._ROW_SCHEDULE]
|
sched = win._page_widgets[win._ROW_SCHEDULE]
|
||||||
from PySide6.QtWidgets import QScrollArea
|
from PySide6.QtWidgets import QScrollArea
|
||||||
lanes = list(sched.columns.values())
|
# R08-T11 moved the Kanban lanes onto KanbanBoardWidget; the shell only
|
||||||
|
# holds the header and the view switch.
|
||||||
|
lanes = list(control(sched, "columns").values())
|
||||||
# The page holds more than one scroll area — take the one the lanes live in.
|
# The page holds more than one scroll area — take the one the lanes live in.
|
||||||
board = next(sa for sa in sched.findChildren(QScrollArea)
|
board = next(sa for sa in sched.findChildren(QScrollArea)
|
||||||
if sa.isAncestorOf(lanes[0]))
|
if sa.isAncestorOf(lanes[0]))
|
||||||
@@ -141,15 +146,16 @@ def main() -> int:
|
|||||||
for _ in range(8):
|
for _ in range(8):
|
||||||
app.processEvents()
|
app.processEvents()
|
||||||
dash = win._page_widgets[win._ROW_DASHBOARD]
|
dash = win._page_widgets[win._ROW_DASHBOARD]
|
||||||
hero, small = dash.card_cost, dash.card_total
|
# R08-T13 moved the stat cards onto TokenUsageCardWidget.
|
||||||
|
hero, small = control(dash, "card_cost"), control(dash, "card_total")
|
||||||
print(f"the Chi phi : x={left_of(hero, dash)} cao={hero.height()} · "
|
print(f"the Chi phi : x={left_of(hero, dash)} cao={hero.height()} · "
|
||||||
f"the phu x={left_of(small, dash)} cao={small.height()}")
|
f"the phu x={left_of(small, dash)} cao={small.height()}")
|
||||||
if left_of(hero, dash) >= left_of(small, dash):
|
if left_of(hero, dash) >= left_of(small, dash):
|
||||||
fails.append("the Chi phi khong nam ben trai cac the phu")
|
fails.append("the Chi phi khong nam ben trai cac the phu")
|
||||||
if hero.height() < small.height() * 1.5:
|
if hero.height() < small.height() * 1.5:
|
||||||
fails.append("the Chi phi khong cao gap ruoi the phu")
|
fails.append("the Chi phi khong cao gap ruoi the phu")
|
||||||
row1 = top_of(dash.card_total, dash)
|
row1 = top_of(control(dash, "card_total"), dash)
|
||||||
row2 = top_of(dash.card_out, dash)
|
row2 = top_of(control(dash, "card_out"), dash)
|
||||||
print(f"the phu hang 1 y={row1} · hang 2 y={row2} (phai la 2 hang)")
|
print(f"the phu hang 1 y={row1} · hang 2 y={row2} (phai la 2 hang)")
|
||||||
if row2 <= row1:
|
if row2 <= row1:
|
||||||
fails.append("4 the phu khong xep 2x2")
|
fails.append("4 the phu khong xep 2x2")
|
||||||
@@ -159,7 +165,7 @@ def main() -> int:
|
|||||||
for _ in range(8):
|
for _ in range(8):
|
||||||
app.processEvents()
|
app.processEvents()
|
||||||
dock = win.help_agent
|
dock = win.help_agent
|
||||||
comp = ws._cowork.composer
|
comp = control(ws._cowork, "composer")
|
||||||
dock_bottom = top_of(dock, win) + dock.height()
|
dock_bottom = top_of(dock, win) + dock.height()
|
||||||
comp_top = top_of(comp, win)
|
comp_top = top_of(comp, win)
|
||||||
print(f"cham {dock.width()}x{dock.height()} · day y={dock_bottom} · o nhap dinh y={comp_top}")
|
print(f"cham {dock.width()}x{dock.height()} · day y={dock_bottom} · o nhap dinh y={comp_top}")
|
||||||
@@ -183,11 +189,12 @@ def main() -> int:
|
|||||||
app.processEvents()
|
app.processEvents()
|
||||||
import cowork_local.ui.co4e_tab as co4e_mod
|
import cowork_local.ui.co4e_tab as co4e_mod
|
||||||
c4 = win.findChildren(co4e_mod.Co4ETab)[0]
|
c4 = win.findChildren(co4e_mod.Co4ETab)[0]
|
||||||
xs = [c4._split.widget(i).x() for i in range(c4._split.count())]
|
c4_split = control(c4, "_split")
|
||||||
|
xs = [c4_split.widget(i).x() for i in range(c4_split.count())]
|
||||||
print(f"Co4E 3 pane x = {xs}")
|
print(f"Co4E 3 pane x = {xs}")
|
||||||
if xs != sorted(xs):
|
if xs != sorted(xs):
|
||||||
fails.append("thu tu 3 pane cua Co4E sai (trai → giua → phai)")
|
fails.append("thu tu 3 pane cua Co4E sai (trai → giua → phai)")
|
||||||
heads = [h.text() for h, _b, _s in c4._sections.values()]
|
heads = [h.text() for h, _b, _s in (control(c4, "_sections") or {}).values()]
|
||||||
print(f"cot sidebar: {heads}")
|
print(f"cot sidebar: {heads}")
|
||||||
if len(heads) != 4:
|
if len(heads) != 4:
|
||||||
fails.append(f"cot sidebar co {len(heads)} muc, ban ve ve 4")
|
fails.append(f"cot sidebar co {len(heads)} muc, ban ve ve 4")
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ MUTATIONS = [
|
|||||||
"ui/help_agent_widget.py", "_DOT = 52", "_DOT = 104",
|
"ui/help_agent_widget.py", "_DOT = 52", "_DOT = 104",
|
||||||
"check_layout_geometry.py"),
|
"check_layout_geometry.py"),
|
||||||
("tra lane Running ve khong vien",
|
("tra lane Running ve khong vien",
|
||||||
"ui/schedule_task_tab.py",
|
# R08-T11 doi cho: ScheduleTaskTab tach ra, phan ve Kanban (ke ca vien
|
||||||
|
# canh bao cua lane Running) nam o presentation/scheduling/.
|
||||||
|
"presentation/scheduling/kanban_board_widget.py",
|
||||||
'if status == "running" and counts[status]:',
|
'if status == "running" and counts[status]:',
|
||||||
'if False:',
|
'if False:',
|
||||||
"check_design_parity.py"),
|
"check_design_parity.py"),
|
||||||
|
|||||||
Reference in New Issue
Block a user