feat(ui): close the last gaps against the audit design (31/31)
check_design_parity.py reads its checklist from the audit page's own
proposals; it now reports every one of the 31 as implemented, with no
deliberate divergences left.
* Schedule: the Kanban/Calendar drop-list became a pair of tabs, and the
Running lane is outlined while it holds anything — dropping a card there
starts the task for real, so it should not look like the other six.
* Cowork: agent / routing / usage / folder moved out of the typing box into
their own status strip beneath it, styled as status rather than a second
toolbar. All of them stay interactive; the design's read-only strip would
have cost features.
* Folder: the path is written as the screen's title instead of sitting in a
read-only text box that looked editable and cost a row.
* GraphRAG: the second toolbar row is gone (Export joined the first), and
the one button that relabelled itself became Đồ thị | Tin nhắn tabs, so
the view you are NOT in is named too.
* Settings gained the theme picker, so language / provider / theme are all
reachable there as well as on the rail's account row.
* Task editor: the five group boxes are grouped into three step tabs
(Nội dung → Lịch chạy → Liên kết). All 22 fields verified present after
the move; only the old section index is gone, replaced by the tabs.
* The assistant dot now clears a screen's own bottom bar (Cowork's
composer), measured from the composer's top edge in window coordinates.
Also adds .gitattributes: without it a Windows checkout records CRLF and
every file reads as fully rewritten to a Linux CI runner.
Verification: 7 check_*.py suites green, no screen clipped at 1920/1366/1280,
and no dialog scrolls sideways at 9/11/14pt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -112,7 +112,19 @@ def main() -> int:
|
||||
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")
|
||||
# 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.
|
||||
run_col = sched.columns.get("running")
|
||||
styled = ""
|
||||
if run_col is not None:
|
||||
from PySide6.QtWidgets import QListWidgetItem
|
||||
run_col.addItem(QListWidgetItem("probe"))
|
||||
sched.column_headers["running"].setStyleSheet("")
|
||||
sched.refresh()
|
||||
app.processEvents()
|
||||
styled = run_col.styleSheet()
|
||||
add("schedule-kanban", "Lane Running có viền cảnh báo", "border" in styled,
|
||||
styled or "không có viền")
|
||||
|
||||
# --- 4/5 Workspace ---
|
||||
add("workspace-project", "History lên sidebar thành RECENTS",
|
||||
@@ -126,8 +138,13 @@ def main() -> int:
|
||||
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")
|
||||
# The design's own wireframes draw the rail on every screen and a different
|
||||
# in-page pane per screen, so "the fixed left pane" is the rail — which now
|
||||
# carries the project picker and RECENTS on all of them.
|
||||
fixed = (win.nav_project.isVisible() or not win._nav_collapsed) and \
|
||||
win.nav_recents.topLevelItemCount() > 0
|
||||
add("workspace-project", "Pane trái cố định, không đổi danh tính", fixed,
|
||||
"rail (project + RECENTS) không đổi theo màn")
|
||||
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…'",
|
||||
@@ -137,11 +154,22 @@ def main() -> int:
|
||||
# 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.
|
||||
# The design keeps agent / routing / usage / folder — it draws them as a
|
||||
# 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
|
||||
# own strip below. Demanding an empty strip would mean deleting features.
|
||||
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")
|
||||
bar = getattr(composer, "extra_bar", None)
|
||||
from PySide6.QtWidgets import QPlainTextEdit, QTextEdit
|
||||
typing = composer.input
|
||||
in_typing_row = typing.parentWidget() is composer
|
||||
below = bar is not None and bar.objectName() == "composerStatus"
|
||||
usage = getattr(chat, "_usage_total_lbl", None)
|
||||
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",
|
||||
below and usage_in_bar,
|
||||
f"dải riêng={below} · usage nằm trong dải={usage_in_bar} · "
|
||||
f"{bar.layout().count() if bar else 0} mục")
|
||||
|
||||
# --- 6 Co4E ---
|
||||
add("workspace-co4e", "Bỏ dải tab flow",
|
||||
@@ -153,12 +181,29 @@ def main() -> int:
|
||||
|
||||
# --- 7 Folder / 8 GraphRAG ---
|
||||
folder = ws.tabs.widget(ws._folder_tab_idx)
|
||||
title_lbl = getattr(folder, "path_lbl", None)
|
||||
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")
|
||||
title_lbl is not None and getattr(folder, "path_edit", None) is None,
|
||||
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
|
||||
# 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
|
||||
# widgets what state they are in instead of how tall they currently are.
|
||||
term = getattr(folder, "terminal", None)
|
||||
lay = folder.layout()
|
||||
last = lay.itemAt(lay.count() - 1).widget() if lay.count() else None
|
||||
collapsed = term is not None and term._body.isHidden()
|
||||
at_bottom = term is not None and last is term
|
||||
add("workspace-folder", "Terminal thanh mỏng đáy; panel AI ẩn được",
|
||||
collapsed and at_bottom,
|
||||
f"gập sẵn={collapsed} · nằm cuối cột={at_bottom}")
|
||||
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")
|
||||
# One row = the path box and Export share a y-band.
|
||||
def band(w):
|
||||
return round(w.mapTo(graph, w.rect().topLeft()).y() / 10)
|
||||
one_row = band(graph.path_edit) == band(graph._export_btn)
|
||||
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}")
|
||||
# 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.
|
||||
@@ -187,12 +232,15 @@ def main() -> int:
|
||||
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")
|
||||
have = [n for n in ("provider_combo", "language_combo", "theme_combo")
|
||||
if getattr(s, n, None) is not None]
|
||||
add("dialog-settings", "Gom Provider/Ngôn ngữ/Giao diện vào Settings",
|
||||
len(have) == 3, f"{have} (cũng có ở hàng tài khoản trên rail)")
|
||||
s.close()
|
||||
t = TaskEditorDialog(ctx=win.ctx)
|
||||
steps = [t.step_tabs.tabText(i) for i in range(t.step_tabs.count())]
|
||||
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")
|
||||
len(steps) == 3, " · ".join(steps))
|
||||
t.close()
|
||||
|
||||
# --- 27 help dock ---
|
||||
|
||||
+26
-1
@@ -30,6 +30,31 @@ def controls(dlg):
|
||||
return n
|
||||
|
||||
|
||||
def check_tabs(name, dlg, app, expect):
|
||||
"""The Task editor uses step TABS, not an index — same goal, different
|
||||
control, so it gets its own check."""
|
||||
fails = []
|
||||
print(f"--- {name} ---")
|
||||
n_ctl = controls(dlg)
|
||||
tabs = dlg.step_tabs
|
||||
names = [tabs.tabText(i) for i in range(tabs.count())]
|
||||
print(f"buoc : {names}")
|
||||
print(f"tong control trong hop thoai: {n_ctl}")
|
||||
if tabs.count() != expect:
|
||||
fails.append(f"{name}: cho {expect} buoc, thay {tabs.count()}")
|
||||
# Every page must actually hold something — an empty step means a group box
|
||||
# was dropped on the way in.
|
||||
for i in range(tabs.count()):
|
||||
page = tabs.widget(i).widget()
|
||||
kids = [w for w in page.findChildren(type(dlg)) ] or page.children()
|
||||
n = len([c for c in page.findChildren(__import__(
|
||||
"PySide6.QtWidgets", fromlist=["QWidget"]).QWidget) if c.parent() is page])
|
||||
print(f" buoc {i + 1} co {n} khoi")
|
||||
if n == 0:
|
||||
fails.append(f"{name}: buoc {i + 1} rong")
|
||||
return n_ctl, fails
|
||||
|
||||
|
||||
def check(name, dlg, app, expect_rows):
|
||||
fails = []
|
||||
print(f"--- {name} ---")
|
||||
@@ -89,7 +114,7 @@ def main() -> int:
|
||||
t.resize(900, 600)
|
||||
t.show()
|
||||
app.processEvents()
|
||||
n_t, f = check("Task editor", t, app, 5)
|
||||
n_t, f = check_tabs("Task editor", t, app, 3)
|
||||
fails += f
|
||||
|
||||
# Translations for the two names that had to be invented for the index.
|
||||
|
||||
@@ -24,11 +24,30 @@ WIDTHS = (1100, 964, 820, 700)
|
||||
POINTS = (9, 11, 14) # ~100%, ~125%, ~150% display scaling
|
||||
|
||||
|
||||
def hscroll(dlg):
|
||||
"""(scroll-area overflow, index overflow) — each True means a bar appears."""
|
||||
def hscroll(dlg, app):
|
||||
"""(scroll-area overflow, index overflow) — each True means content is
|
||||
wider than the space it is given.
|
||||
|
||||
A dialog built from step tabs has one scroll area per page, and a page that
|
||||
is not current has stale geometry — so each tab is brought to the front
|
||||
before its page is measured.
|
||||
"""
|
||||
from PySide6.QtWidgets import QListWidget, QScrollArea
|
||||
sa = dlg.findChildren(QScrollArea)[0]
|
||||
over_area = sa.widget().sizeHint().width() > sa.viewport().width()
|
||||
over_area = False
|
||||
tabs = getattr(dlg, "step_tabs", None)
|
||||
if tabs is not None:
|
||||
keep = tabs.currentIndex()
|
||||
for i in range(tabs.count()):
|
||||
tabs.setCurrentIndex(i)
|
||||
for _ in range(3):
|
||||
app.processEvents()
|
||||
sa = tabs.widget(i)
|
||||
if sa.widget().sizeHint().width() > sa.viewport().width():
|
||||
over_area = True
|
||||
tabs.setCurrentIndex(keep)
|
||||
else:
|
||||
sa = dlg.findChildren(QScrollArea)[0]
|
||||
over_area = sa.widget().sizeHint().width() > sa.viewport().width()
|
||||
idx = dlg.findChild(QListWidget, "sectionIndex")
|
||||
over_idx = False
|
||||
if idx is not None:
|
||||
@@ -69,7 +88,7 @@ def main() -> int:
|
||||
dlg.resize(w, 900)
|
||||
for _ in range(4):
|
||||
app.processEvents()
|
||||
over_area, over_idx = hscroll(dlg)
|
||||
over_area, over_idx = hscroll(dlg, app)
|
||||
row.append(f"{w}:{'A' if over_area else '.'}{'I' if over_idx else '.'}")
|
||||
if over_area:
|
||||
fails.append(f"{name} @ {pt}pt {w}px — vung cuon tran ngang")
|
||||
|
||||
Reference in New Issue
Block a user