diff --git a/.cowork_history/cowork__20260818-130950-831.json b/.cowork_history/cowork__20260818-130950-831.json new file mode 100644 index 0000000..b137cb1 --- /dev/null +++ b/.cowork_history/cowork__20260818-130950-831.json @@ -0,0 +1,16 @@ +{ + "kind": "cowork", + "session_id": "20260818-130950-831", + "title": "alo", + "created": "2026-08-18T13:10:03", + "pinned": false, + "project_id": "project-mới", + "inputs": [], + "outputs": [], + "messages": [ + { + "role": "user", + "content": "alo" + } + ] +} \ No newline at end of file diff --git a/i18n.py b/i18n.py index c4112cc..c4fc6f2 100644 --- a/i18n.py +++ b/i18n.py @@ -2472,8 +2472,8 @@ STRINGS: Dict[str, Dict[str, str]] = { "下から独自のSVGアイコンを追加でき、名前ですぐ使えます。", "vi": "Các icon dùng cho agent và flow. Gõ tên vào ô Icon của step/agent để dùng. Thêm icon SVG " "của bạn ở dưới — dùng được ngay bằng tên."}, - "icons_admin.search": {"en": "Search built-in icons…", "ja": "組込みアイコンを検索…", "vi": "Tìm icon có sẵn…"}, - "icons_admin.builtin": {"en": "Built-in icons", "ja": "組込みアイコン", "vi": "Icon có sẵn"}, + "icons_admin.search": {"en": "Search icons by name…", "ja": "名前でアイコンを検索…", "vi": "Tìm icon theo tên…"}, + "icons_admin.builtin": {"en": "Built-in icons", "ja": "組込みアイコン", "vi": "Icon tích hợp"}, "icons_admin.custom": {"en": "Custom icons", "ja": "カスタムアイコン", "vi": "Icon tùy chỉnh"}, "icons_admin.add": {"en": "Add SVG file", "ja": "SVGファイルを追加", "vi": "Thêm tệp SVG"}, "icons_admin.paste": {"en": "Paste SVG", "ja": "SVGを貼付", "vi": "Dán SVG"}, diff --git a/theme.py b/theme.py index 5bbe87b..473e521 100644 --- a/theme.py +++ b/theme.py @@ -458,6 +458,23 @@ QLabel#navSectionHdr { padding: 8px 8px 2px 8px; background: transparent; } QTreeWidget#navRecents { border-top: 1px solid $nav_border; } + +/* Icon library cells. The audit page's note on this screen is that the cells + had no visible edge on hover or selection, so you could not tell what you + were about to pick — "cùng ngôn ngữ thẻ với Agent/Công cụ". */ +QListWidget#iconGrid { background: transparent; border: none; } +QListWidget#iconGrid::item { + border: 1px solid transparent; border-radius: ${radius}px; + color: $text_muted; padding: 4px; +} +QListWidget#iconGrid::item:hover { + border: 1px solid $accent; background: $hover; color: $text; +} +QListWidget#iconGrid::item:selected { + border: 1px solid $accent; background: $accent_wash; color: $text; +} +/* Screen title beside its actions, same weight the other admin screens use. */ +QLabel#monTitle { font-size: 15px; font-weight: 700; color: $text; } /* Rail header — the primary action, so it is the one filled button up there. */ QPushButton#navNewChatBtn { background: $accent_solid; color: #FFFFFF; border: none; font-weight: 600; diff --git a/tools/check_icons_screen.py b/tools/check_icons_screen.py new file mode 100644 index 0000000..4dcf57f --- /dev/null +++ b/tools/check_icons_screen.py @@ -0,0 +1,108 @@ +"""The Icon library screen against its wireframe. + +The drawing (section 16) puts the three actions on the title row, a magnifier +in the search box, caps section headings, and — its stated complaint — a +visible edge on an icon cell when you hover or select it, so you can tell what +you are about to pick. +""" +from __future__ import annotations + +import os +import sys + +sys.stdout.reconfigure(encoding="utf-8", errors="replace") + +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 ( # noqa: E402 + _apply_theme, _freeze_schedulers, _isolate_home, _load_fonts) + + +def main() -> int: + sandbox = _isolate_home() + from PySide6.QtCore import QPoint + from PySide6.QtWidgets import QApplication, QLineEdit + + app = QApplication([]) + _load_fonts() + _freeze_schedulers() + _apply_theme(app) + + from cowork_local.config import CONFIG_DIR + assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}" + + from cowork_local.config import AppConfig + from cowork_local.i18n import set_language + from cowork_local.state import AppContext + from cowork_local.ui.icons_admin_tab import IconsAdminTab + + set_language("vi") + tab = IconsAdminTab(AppContext(AppConfig.load())) + tab.resize(1100, 700) + tab.show() + app.processEvents() + + fails = [] + + # 1. actions on the title row, to its right — not in a strip below the + # grids. Comparing y alone was not enough: a button left out of the + # layout sits at (0,0), which is "the same row" by accident. + title = tab._title + t_pos = title.mapTo(tab, QPoint(0, 0)) + t_mid = t_pos.y() + title.height() // 2 + t_right = t_pos.x() + title.width() + for name, btn in (("Thêm", tab.add_btn), ("Dán", tab.paste_btn), + ("Xóa", tab.del_btn)): + pos = btn.mapTo(tab, QPoint(0, 0)) + mid = pos.y() + btn.height() // 2 + aligned = abs(mid - t_mid) <= 6 + after = pos.x() >= t_right + print(f"nut {name:5}: tam y={mid} (tieu de {t_mid}) thang hang={aligned} " + f"| x={pos.x()} (sau tieu de {t_right})={after}") + if not (aligned and after): + fails.append(f"nut {name} khong nam cung hang, ben phai tieu de") + + # 2. magnifier in the search box + lead = tab.search.actions() + print(f"o tim co icon kinh lup: {bool(lead)}") + if not lead: + fails.append("o tim thieu icon kinh lup") + + # 3. caps headings + for name, lbl in (("tich hop", tab._builtin_lbl), ("tuy chinh", tab._custom_lbl)): + text = lbl.text() + print(f"tieu de {name}: {text!r}") + if text != text.upper(): + fails.append(f"tieu de {name} chua viet hoa: {text!r}") + + # 4. the cell has an edge to see — compare the painted cell hovered vs not + grid = tab.builtin_grid + if grid.count(): + rect = grid.visualItemRect(grid.item(0)) + plain = grid.grab(rect).toImage() + grid.setCurrentRow(0) + app.processEvents() + picked = grid.grab(rect).toImage() + diff = sum(1 for x in range(plain.width()) for y in range(plain.height()) + if plain.pixelColor(x, y) != picked.pixelColor(x, y)) + print(f"o icon doi {diff} diem anh khi duoc chon") + if diff == 0: + fails.append("o icon khong doi gi khi duoc chon — khong thay minh dang chon cai nao") + else: + fails.append("luoi icon rong") + + print() + for f in fails: + print("FAIL " + f) + print("PASS man Icon khop ban ve" if not fails else f"{len(fails)} problem(s)") + sys.stdout.flush() + os._exit(1 if fails else 0) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/ui/icons_admin_tab.py b/ui/icons_admin_tab.py index 6ba27d2..6e43a2d 100644 --- a/ui/icons_admin_tab.py +++ b/ui/icons_admin_tab.py @@ -22,6 +22,7 @@ from .icons import icon def _grid() -> QListWidget: g = QListWidget() + g.setObjectName("iconGrid") # accent border on hover/selection, see theme.py g.setViewMode(QListWidget.IconMode) g.setResizeMode(QListWidget.Adjust) g.setMovement(QListWidget.Static) @@ -36,34 +37,44 @@ class IconsAdminTab(QWidget): super().__init__() self.ctx = ctx root = QVBoxLayout(self) - self._hint = QLabel(); self._hint.setObjectName("hint"); self._hint.setWordWrap(True) - root.addWidget(self._hint) - - # search over built-in names - self.search = QLineEdit() - self.search.textChanged.connect(self._reload_builtin) - root.addWidget(self.search) - - self._builtin_lbl = QLabel() - root.addWidget(self._builtin_lbl) - self.builtin_grid = _grid() - root.addWidget(self.builtin_grid, 2) - - self._custom_lbl = QLabel() - root.addWidget(self._custom_lbl) - self.custom_grid = _grid() - root.addWidget(self.custom_grid, 1) - - btns = QHBoxLayout() + # Header row: the three actions sit beside the title, where the drawing + # puts them, instead of in a strip below the two grids where they read + # as belonging to the custom grid alone. + head = QHBoxLayout() + self._title = QLabel() + self._title.setObjectName("monTitle") + head.addWidget(self._title) + head.addStretch(1) self.add_btn = QPushButton(); self.add_btn.setIcon(icon("plus")) self.add_btn.clicked.connect(self._add_icon) self.paste_btn = QPushButton() self.paste_btn.clicked.connect(self._add_from_svg_text) self.del_btn = QPushButton(); self.del_btn.setIcon(icon("trash")) self.del_btn.clicked.connect(self._delete_icon) - btns.addWidget(self.add_btn); btns.addWidget(self.paste_btn) - btns.addWidget(self.del_btn); btns.addStretch(1) - root.addLayout(btns) + for b in (self.add_btn, self.paste_btn, self.del_btn): + head.addWidget(b) + root.addLayout(head) + + self._hint = QLabel(); self._hint.setObjectName("hint"); self._hint.setWordWrap(True) + root.addWidget(self._hint) + + # search over built-in names, with the magnifier the drawing asks for + self.search = QLineEdit() + self.search.addAction(icon("search"), QLineEdit.LeadingPosition) + self.search.textChanged.connect(self._reload_builtin) + root.addWidget(self.search) + + self._builtin_lbl = QLabel() + self._builtin_lbl.setObjectName("navSectionHdr") # quiet caps heading + root.addWidget(self._builtin_lbl) + self.builtin_grid = _grid() + root.addWidget(self.builtin_grid, 2) + + self._custom_lbl = QLabel() + self._custom_lbl.setObjectName("navSectionHdr") + root.addWidget(self._custom_lbl) + self.custom_grid = _grid() + root.addWidget(self.custom_grid, 1) on_language_changed(self._retranslate) self._retranslate() @@ -131,10 +142,13 @@ class IconsAdminTab(QWidget): self._reload_custom() def _retranslate(self) -> None: + self._title.setText(tr("monitoring.tab_icons")) self._hint.setText(tr("icons_admin.hint")) self.search.setPlaceholderText(tr("icons_admin.search")) - self._builtin_lbl.setText(tr("icons_admin.builtin")) - self._custom_lbl.setText(tr("icons_admin.custom")) + # ICON TÍCH HỢP / ICON TÙY CHỈNH — caps, like every other section + # heading the audit page draws. + self._builtin_lbl.setText(tr("icons_admin.builtin").upper()) + self._custom_lbl.setText(tr("icons_admin.custom").upper()) self.add_btn.setText(tr("icons_admin.add")) self.paste_btn.setText(tr("icons_admin.paste")) self.del_btn.setText(tr("icons_admin.delete"))