docs(agent): bổ sung role fix-dispatcher và siết lại bộ tài liệu agent
- Thêm agent/roles/0_fix_dispatcher.md: phân tier/lane cho từng defect trước khi các agent khác chạy, kèm agent/commands/fix.md và hợp đồng đầu ra agent/output/dispatch_plan.md. - Cập nhật system/guardrail, response_policy, security và các checklist ui/ux/pr_readiness cho khớp luồng mới. - Mở rộng knowledge: i18n_rules, screen_map, theme_tokens, secrets_and_config; cập nhật workflow intake_to_fix và handoff_contract. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+354
-52
@@ -1,71 +1,373 @@
|
||||
# i18n — luật chuỗi hiển thị
|
||||
# i18n — Quy tắc xử lý chuỗi hiển thị
|
||||
|
||||
Nguồn: docstring `i18n/__init__.py`.
|
||||
**Nguồn:** docstring `i18n/__init__.py`
|
||||
|
||||
---
|
||||
|
||||
## 1. Ba ngôn ngữ, mặc định tiếng Việt
|
||||
## 1. Ngôn ngữ được hỗ trợ
|
||||
|
||||
Cowork Local hỗ trợ 3 ngôn ngữ:
|
||||
|
||||
```python
|
||||
LANGUAGES = {"en": "English", "ja": "日本語", "vi": "Tiếng Việt"}
|
||||
LANGUAGE_SHORT = {"en": "EN", "ja": "JP", "vi": "VN"} # switcher gọn ở top bar
|
||||
LANGUAGES = {
|
||||
"en": "English",
|
||||
"ja": "日本語",
|
||||
"vi": "Tiếng Việt",
|
||||
}
|
||||
|
||||
LANGUAGE_SHORT = {
|
||||
"en": "EN",
|
||||
"ja": "JP",
|
||||
"vi": "VN",
|
||||
}
|
||||
|
||||
DEFAULT_LANGUAGE = "vi"
|
||||
```
|
||||
|
||||
`tr(key, **kwargs)` trả chuỗi theo ngôn ngữ hiện tại, fallback lần lượt:
|
||||
**ngôn ngữ hiện tại → `en` → chính cái key**. Nghĩa là thiếu entry thì UI hiện ra
|
||||
`workspace.tab_folder` chứ không crash — nếu người dùng chụp màn hình có chuỗi dạng
|
||||
`a.b_c` thì đó chính là triệu chứng thiếu key.
|
||||
Ngôn ngữ mặc định là **Tiếng Việt (`vi`)**.
|
||||
|
||||
`.format(**kwargs)` được áp dụng khi có placeholder: `tr("composer.attachments", n=3)`.
|
||||
### Hàm `tr()`
|
||||
|
||||
## 2. Widget nào phải đăng ký callback
|
||||
Sử dụng:
|
||||
|
||||
| Loại widget | Cách xử lý |
|
||||
|---|---|
|
||||
| **Sống lâu** — chrome cửa sổ chính, tab, sidebar, composer | Đăng ký `on_language_changed(cb)`; `cb` áp lại `tr()` cho chính widget đó. Callback chạy **ngay một lần** và mỗi lần đổi ngôn ngữ |
|
||||
| **Tạm thời** — Settings, Skills, Flow, Permission dialog | Dựng lại từ đầu mỗi lần mở, nên chỉ cần gọi `tr()` lúc construct, **không** đăng ký |
|
||||
|
||||
Quy ước đặt tên hàm callback trong repo: `_retranslate()` / `_apply_i18n()` — xem
|
||||
`ui/workspace_tab.py:484` trở đi làm mẫu chuẩn.
|
||||
|
||||
**Bug điển hình:** "Đổi ngôn ngữ nhưng nhãn X không đổi" → widget sống lâu mà quên đăng ký,
|
||||
hoặc có đăng ký nhưng callback bỏ sót đúng nhãn đó. Không sửa bằng cách gọi `tr()` lại ở
|
||||
chỗ khác — sửa trong callback.
|
||||
|
||||
## 3. File từ điển
|
||||
|
||||
`i18n/` chia theo màn hình, không phải một file khổng lồ:
|
||||
|
||||
```text
|
||||
i18n/login_dialog.py i18n/sidebar.py i18n/composer.py
|
||||
i18n/cowork_tab.py i18n/settings_dialog.py i18n/skills_dialog.py
|
||||
i18n/libreoffice_view.py i18n/agents_admin_tab.py i18n/monitoring_overview.py
|
||||
i18n/hint.py
|
||||
```python
|
||||
tr(key, **kwargs)
|
||||
```
|
||||
|
||||
Mỗi file export dict `key -> {"en":..., "ja":..., "vi":...}`, được `i18n/__init__.py`
|
||||
import và gộp lại. Thêm key mới:
|
||||
để lấy chuỗi hiển thị theo ngôn ngữ hiện tại.
|
||||
|
||||
1. Chọn đúng file theo màn hình (không nhét đại vào `login_dialog.py` chỉ vì nó lớn nhất).
|
||||
2. Điền **đủ 3 ngôn ngữ**. Thiếu `ja` là lỗi hay gặp nhất và chỉ lộ ra khi khách Nhật dùng.
|
||||
3. Đặt key theo `<màn>.<thành_phần>` — `workspace.tab_folder`, `app.nav.recents`.
|
||||
Thứ tự fallback:
|
||||
|
||||
## 4. Rủi ro riêng của tiếng Nhật và tiếng Việt
|
||||
```text
|
||||
Ngôn ngữ hiện tại → English (en) → chính key
|
||||
```
|
||||
|
||||
| Rủi ro | Triệu chứng | Cách xử lý |
|
||||
|---|---|---|
|
||||
| Tiếng Nhật ngắn hơn, tiếng Việt dài hơn tiếng Anh | Nút vừa với `EN`, tràn với `VI`; label bị `...` với `JA` | Không `setFixedWidth` theo chuỗi tiếng Anh. Dùng `sizeHint` + `minimumWidth`, hoặc cho phép wrap |
|
||||
| Dấu tiếng Việt bị cắt phần trên/dưới | `Ắ`, `ộ` mất dấu ở nhãn cao cố định | Không đặt `setFixedHeight` cho label theo pixel; để layout tự tính |
|
||||
| Font mặc định thiếu glyph Nhật | Ô vuông tofu `□□□` trên máy chưa cài font | Kiểm tra `_FONT` trong `theme/palettes.py`, khai báo fallback |
|
||||
| Sắp xếp / so sánh chuỗi | Danh sách project sắp sai với tên có dấu | Dùng `locale`-aware sort, không `sorted()` thô |
|
||||
| Chiều dài chuỗi tính bằng ký tự ≠ chiều rộng hiển thị | Elide sai với chữ Nhật | Đo bằng `QFontMetrics.horizontalAdvance`, không `len()` |
|
||||
Ví dụ, nếu đang dùng tiếng Nhật nhưng key `workspace.tab_folder` chưa có bản dịch tiếng Nhật:
|
||||
|
||||
## 5. Checklist sửa bug i18n
|
||||
```text
|
||||
JA → EN → workspace.tab_folder
|
||||
```
|
||||
|
||||
- [ ] Key mới có đủ `en` / `ja` / `vi`?
|
||||
- [ ] Đã thử đổi qua cả 3 ngôn ngữ **trong lúc app đang chạy** (không phải restart)?
|
||||
- [ ] Widget sống lâu đã đăng ký `on_language_changed`?
|
||||
- [ ] Không còn chuỗi hardcode nào trong bản vá?
|
||||
- [ ] Layout còn đúng với chuỗi dài nhất trong 3 ngôn ngữ?
|
||||
- [ ] Không dùng `len()` để đo bề rộng chữ?
|
||||
Ứng dụng **không được crash** chỉ vì thiếu bản dịch.
|
||||
|
||||
Nếu UI hiển thị một chuỗi dạng:
|
||||
|
||||
```text
|
||||
workspace.tab_folder
|
||||
```
|
||||
|
||||
thì đây là dấu hiệu cho thấy **đang thiếu translation key**.
|
||||
|
||||
### Placeholder
|
||||
|
||||
Nếu chuỗi có placeholder, truyền giá trị thông qua `kwargs`:
|
||||
|
||||
```python
|
||||
tr("composer.attachments", n=3)
|
||||
```
|
||||
|
||||
Việc `.format(**kwargs)` được thực hiện sau khi lấy chuỗi dịch.
|
||||
|
||||
---
|
||||
|
||||
## 2. Widget nào phải cập nhật khi đổi ngôn ngữ?
|
||||
|
||||
Có 2 loại widget:
|
||||
|
||||
| Loại widget | Cách xử lý |
|
||||
| ------------------- | ----------------------------------------------------- |
|
||||
| **Widget sống lâu** | `bind_*` cho chuỗi tĩnh; `on_language_changed(cb)` cho phần còn lại |
|
||||
| **Widget tạm thời** | Không cần đăng ký callback; gọi `tr()` khi tạo widget |
|
||||
|
||||
### 2.0. `bind_*` — cách mặc định cho chuỗi tĩnh
|
||||
|
||||
`w.setToolTip(tr("k"))` chỉ đúng ở đúng thời điểm chạy dòng đó. `bind_*` gộp "gán ngay"
|
||||
và "gán lại sau mỗi lần đổi ngôn ngữ" vào một lời gọi, dùng `weakref` nên không giữ widget
|
||||
sống thêm và tự dọn khi widget bị xoá:
|
||||
|
||||
```python
|
||||
from ...i18n import bind_dynamic, bind_items, bind_placeholder, bind_text, bind_tip
|
||||
|
||||
self.save_btn = bind_text(QPushButton(), "co4e.save") # thay QPushButton(tr(...))
|
||||
bind_tip(self.save_btn, "co4e.tt_save") # thay .setToolTip(tr(...))
|
||||
bind_placeholder(self.chat_input, "co4e.chat_placeholder")
|
||||
bind_items(self.perm_combo, [f"co4e.perm.{p}" for p in PERMISSION_PRESETS])
|
||||
form.addRow(bind_text(QLabel(), "co4e.f_label"), self.label_edit) # KHÔNG addRow(tr(...))
|
||||
```
|
||||
|
||||
Ba luật:
|
||||
|
||||
1. **Chuỗi tĩnh → `bind_*`.** Đổi tại chỗ, **không thêm dòng** — quan trọng với file đã
|
||||
sát trần Gate S hoặc đang bị bánh cóc `LEGACY_ALLOWANCE` chốt (`quality_gates.md` §4).
|
||||
2. **Chữ phụ thuộc trạng thái → `bind_dynamic(w, setter, fn)`**, với `fn` đọc trạng thái:
|
||||
nút Chạy ⇄ Dừng, tooltip Thu gọn ⇄ Mở rộng, nhãn có số đếm. Các nhánh xử lý trạng thái
|
||||
**vẫn** gọi setter trực tiếp như cũ để phản hồi ngay khi bấm; `bind_dynamic` chỉ lo lúc
|
||||
đổi ngôn ngữ. Bind cứng một nhãn động sẽ **xoá** trạng thái khi người dùng đổi ngôn ngữ
|
||||
giữa lúc đang chạy.
|
||||
3. **Chữ là DỮ LIỆU thì không bind.** Tên agent, tên project, tên nhà cung cấp trong
|
||||
`config.PROVIDER_LABELS` — dịch danh tính là sai.
|
||||
|
||||
`QFormLayout.addRow(tr(...), w)` và `_add_section(outer, tr(...))` là hai bẫy hay gặp:
|
||||
chúng tự dựng `QLabel` bên trong, không giữ tham chiếu nào để áp lại. Truyền
|
||||
`bind_text(QLabel(), key)` hoặc truyền **khoá** thay vì chuỗi đã dịch.
|
||||
|
||||
### 2.1. Widget sống lâu
|
||||
|
||||
Ví dụ:
|
||||
|
||||
* Chrome của cửa sổ chính.
|
||||
* Tab.
|
||||
* Sidebar.
|
||||
* Composer.
|
||||
|
||||
Các widget này vẫn tồn tại khi người dùng đổi ngôn ngữ.
|
||||
|
||||
Vì vậy phải:
|
||||
|
||||
1. Đăng ký `on_language_changed(cb)`.
|
||||
2. Trong callback, gọi lại `tr()` cho các text của chính widget.
|
||||
3. Callback phải chạy:
|
||||
|
||||
* Một lần ngay khi đăng ký.
|
||||
* Mỗi lần người dùng đổi ngôn ngữ.
|
||||
|
||||
Tên callback được sử dụng trong repo:
|
||||
|
||||
```text
|
||||
_retranslate()
|
||||
_apply_i18n()
|
||||
```
|
||||
|
||||
Có thể tham khảo implementation chuẩn từ:
|
||||
|
||||
```text
|
||||
ui/workspace_tab.py:484
|
||||
```
|
||||
|
||||
### 2.2. Widget tạm thời
|
||||
|
||||
Ví dụ:
|
||||
|
||||
* Settings dialog.
|
||||
* Skills dialog.
|
||||
* Flow dialog.
|
||||
* Permission dialog.
|
||||
|
||||
Các dialog này được tạo lại từ đầu mỗi lần mở.
|
||||
|
||||
Vì vậy chỉ cần gọi `tr()` khi construct widget.
|
||||
|
||||
**Không cần đăng ký `on_language_changed()`**.
|
||||
|
||||
### Bug thường gặp
|
||||
|
||||
Triệu chứng:
|
||||
|
||||
> Đổi ngôn ngữ nhưng một label/nút vẫn giữ ngôn ngữ cũ.
|
||||
|
||||
Nguyên nhân thường là:
|
||||
|
||||
* Widget sống lâu nhưng chưa đăng ký `on_language_changed()`.
|
||||
* Callback có đăng ký nhưng quên cập nhật label đó.
|
||||
|
||||
**Cách sửa đúng:**
|
||||
|
||||
`bind_*` tại chính dòng đang gán (mục 2.0), hoặc — nếu chữ phụ thuộc trạng thái/dữ liệu —
|
||||
sửa trong `_retranslate()` / `_apply_i18n()` của chính widget.
|
||||
|
||||
**Không** giải quyết bằng cách gọi `tr()` ở một nơi khác chỉ để ép label thay đổi.
|
||||
|
||||
### Cách TÌM ra hết các chỗ bị lỗi
|
||||
|
||||
Đừng grep chuỗi tiếng Việt trong source: lượt audit tháng 9/2026 grep ra 962 dòng mà
|
||||
**không dòng nào** là lỗi thật (toàn docstring), trong khi 84 lỗi thật lại không xuất hiện
|
||||
— vì chúng đi qua `tr()` đúng cách, chỉ thiếu người áp lại.
|
||||
|
||||
Phép đo đúng nằm ở `tests/ui/test_i18n_khong_con_chu_cu.py`: dựng `MainWindow` thật, thay
|
||||
`tr()` bằng chuỗi **mốc**, gọi `set_language()`, rồi tìm chỗ **không** mang mốc. Hai chi
|
||||
tiết mà bản kiểm ngây thơ sẽ sai:
|
||||
|
||||
* `from ...i18n import tr` copy tham chiếu vào namespace từng module → phải thay `tr` ở
|
||||
**mọi** module đã import, không chỉ `i18n.tr`;
|
||||
* lưới vẽ lại bằng `deleteLater()` để lại widget cũ còn sống → không
|
||||
`sendPostedEvents(DeferredDelete)` thì báo oan hàng chục widget bóng ma (lượt audit đầu
|
||||
báo 84 lỗi, trong đó 65 là bóng ma và widget bị `id()` cấp lại làm cắt vòng quét).
|
||||
|
||||
Chạy: `QT_QPA_PLATFORM=offscreen pytest tests/ui/test_i18n_khong_con_chu_cu.py -q`
|
||||
|
||||
---
|
||||
|
||||
## 3. Tổ chức file translation
|
||||
|
||||
Thư mục `i18n/` được chia theo **màn hình/chức năng**, không gom tất cả translation vào một file lớn.
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
i18n/
|
||||
├── login_dialog.py
|
||||
├── sidebar.py
|
||||
├── composer.py
|
||||
├── cowork_tab.py
|
||||
├── settings_dialog.py
|
||||
├── skills_dialog.py
|
||||
├── libreoffice_view.py
|
||||
├── agents_admin_tab.py
|
||||
├── monitoring_overview.py
|
||||
└── hint.py
|
||||
```
|
||||
|
||||
Mỗi file export một dictionary có dạng:
|
||||
|
||||
```text
|
||||
key → {
|
||||
"en": "...",
|
||||
"ja": "...",
|
||||
"vi": "..."
|
||||
}
|
||||
```
|
||||
|
||||
`i18n/__init__.py` sẽ import và gộp các dictionary này.
|
||||
|
||||
### Khi thêm key mới
|
||||
|
||||
Thực hiện theo 3 bước:
|
||||
|
||||
#### Bước 1 — Chọn đúng file
|
||||
|
||||
Đưa key vào file tương ứng với màn hình/chức năng.
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
workspace.* → file liên quan đến workspace
|
||||
composer.* → composer.py
|
||||
settings.* → settings_dialog.py
|
||||
```
|
||||
|
||||
**Không** đưa key vào `login_dialog.py` chỉ vì file đó đang có nhiều key nhất.
|
||||
|
||||
#### Bước 2 — Điền đủ 3 ngôn ngữ
|
||||
|
||||
Mỗi key mới phải có:
|
||||
|
||||
```text
|
||||
en
|
||||
ja
|
||||
vi
|
||||
```
|
||||
|
||||
Thiếu `ja` là lỗi đặc biệt cần chú ý vì có thể chỉ được phát hiện khi khách hàng Nhật sử dụng.
|
||||
|
||||
#### Bước 3 — Đặt tên key nhất quán
|
||||
|
||||
Format khuyến nghị:
|
||||
|
||||
```text
|
||||
<màn hình>.<thành phần>
|
||||
```
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
workspace.tab_folder
|
||||
app.nav.recents
|
||||
```
|
||||
|
||||
Tên key phải mô tả rõ nó được dùng ở đâu và cho thành phần nào.
|
||||
|
||||
---
|
||||
|
||||
## 4. Các rủi ro thường gặp với tiếng Nhật và tiếng Việt
|
||||
|
||||
| Vấn đề | Triệu chứng | Cách xử lý |
|
||||
| ---------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| Độ dài chuỗi khác nhau | EN vừa nút nhưng VI bị tràn hoặc JA bị `...` | Không đặt width cố định dựa trên tiếng Anh. Dùng `sizeHint()`, `minimumWidth` hoặc cho phép wrap |
|
||||
| Dấu tiếng Việt bị cắt | Các chữ như `Ắ`, `ộ` bị mất dấu | Không dùng `setFixedHeight()` cho label. Để layout tự tính chiều cao |
|
||||
| Thiếu font/glyph tiếng Nhật | Xuất hiện `□□□` | Kiểm tra `_FONT` trong `theme/palettes.py` và khai báo font fallback |
|
||||
| Sắp xếp chuỗi | Project có dấu được sắp xếp không đúng | Dùng locale-aware sorting, không dùng `sorted()` một cách máy móc |
|
||||
| Số ký tự không phản ánh chiều rộng | Text bị elide sai, đặc biệt với tiếng Nhật | Dùng `QFontMetrics.horizontalAdvance()`, không dùng `len()` để đo chiều rộng |
|
||||
|
||||
### Đặc biệt lưu ý về độ dài text
|
||||
|
||||
Không được giả định:
|
||||
|
||||
```text
|
||||
số ký tự = chiều rộng hiển thị
|
||||
```
|
||||
|
||||
Ví dụ hai chuỗi có cùng số ký tự nhưng có thể có chiều rộng hiển thị khác nhau.
|
||||
|
||||
Khi cần đo text trên UI, dùng:
|
||||
|
||||
```python
|
||||
QFontMetrics.horizontalAdvance(...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Checklist khi sửa lỗi i18n
|
||||
|
||||
Trước khi hoàn thành bản vá i18n, phải kiểm tra:
|
||||
|
||||
* [ ] Key mới có đủ **`en` / `ja` / `vi`**?
|
||||
|
||||
* [ ] Đã chuyển qua cả 3 ngôn ngữ **ngay trong lúc app đang chạy** chưa?
|
||||
|
||||
```
|
||||
Không chỉ restart app rồi kiểm tra.
|
||||
```
|
||||
|
||||
* [ ] `ja` có **khác** `en` không? Bằng nhau nghĩa là chưa dịch — trừ tên thương hiệu /
|
||||
ký hiệu, và khi đó phải khai vào `KHOA_KHONG_CAN_DICH` kèm lý do.
|
||||
|
||||
* [ ] Chuỗi tĩnh đã dùng `bind_text` / `bind_tip` / `bind_placeholder` / `bind_items`
|
||||
thay cho `setX(tr(...))` một lần?
|
||||
|
||||
* [ ] Chữ phụ thuộc trạng thái đã dùng `bind_dynamic` (không bind cứng, kẻo mất trạng thái)?
|
||||
|
||||
* [ ] Nếu widget sống lâu và còn phần không bind được, đã đăng ký:
|
||||
|
||||
```
|
||||
`on_language_changed(...)`
|
||||
```
|
||||
|
||||
* [ ] Callback `_retranslate()` hoặc `_apply_i18n()` đã cập nhật **tất cả text liên quan**?
|
||||
|
||||
* [ ] Đã chạy `pytest tests/ui/test_i18n_khong_con_chu_cu.py -q` và nó **xanh**?
|
||||
|
||||
* [ ] Không còn chuỗi hardcode mới trong bản vá?
|
||||
|
||||
* [ ] Layout vẫn đúng với **chuỗi dài nhất** trong 3 ngôn ngữ?
|
||||
|
||||
* [ ] Không dùng `len()` để tính chiều rộng text?
|
||||
|
||||
* [ ] Nếu có thay đổi UI, đã kiểm tra cả Dark Mode và Light Mode?
|
||||
|
||||
---
|
||||
|
||||
## 6. Nguyên tắc quan trọng
|
||||
|
||||
Khi sửa lỗi i18n, **không sửa triệu chứng ở nơi khác**.
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
Đổi ngôn ngữ
|
||||
↓
|
||||
Label X không thay đổi
|
||||
↓
|
||||
Kiểm tra widget X
|
||||
↓
|
||||
Widget sống lâu?
|
||||
↓
|
||||
Có on_language_changed()?
|
||||
↓
|
||||
_retranslate() có cập nhật Label X?
|
||||
```
|
||||
|
||||
Nếu thiếu callback hoặc callback bỏ sót label, hãy sửa **đúng callback của widget đó**.
|
||||
|
||||
Không thêm các lệnh `tr()` rải rác ở nơi khác chỉ để làm cho UI thay đổi.
|
||||
|
||||
Mục tiêu là đảm bảo cơ chế i18n hoạt động đúng và nhất quán cho toàn bộ ứng dụng.
|
||||
|
||||
+443
-58
@@ -1,95 +1,480 @@
|
||||
# Screen Map — dịch lời người dùng thành file:line
|
||||
# Screen Map — Tra mô tả của người dùng về đúng file:line
|
||||
|
||||
Người dùng báo lỗi bằng lời ("cái bảng bên phải màn thống kê"). File này để agent
|
||||
Triage quy nó về đúng widget.
|
||||
Người dùng thường mô tả lỗi bằng ngôn ngữ tự nhiên, ví dụ:
|
||||
|
||||
> "Cái bảng bên phải của màn thống kê bị lệch."
|
||||
|
||||
Agent phải dùng file này để chuyển mô tả đó thành:
|
||||
|
||||
```text
|
||||
Màn hình → Tab/View → Widget → File → Line → Control
|
||||
```
|
||||
|
||||
Mục tiêu là tìm được **đúng widget và đúng vị trí code**, thay vì đoán file dựa trên tên.
|
||||
|
||||
---
|
||||
|
||||
## 1. Nav rail — bốn màn chính
|
||||
## 1. Bốn màn hình chính trong Nav Rail
|
||||
|
||||
Định nghĩa tại `presentation/shell/main_window.py:151` (`_nav_defs`), thứ tự = page index:
|
||||
Các màn hình chính được định nghĩa tại:
|
||||
|
||||
| Row | i18n key | Icon | Dựng | Widget |
|
||||
|---|---|---|---|---|
|
||||
| 0 | `app.tab.dashboard` | `dashboard` | lười | `presentation/dashboard/dashboard_tab.py::DashboardTab` |
|
||||
| 1 | `app.tab.schedule` | `schedule` | lười | `presentation/scheduling/schedule_task_tab.py::ScheduleTaskTab` |
|
||||
| 2 | `app.tab.workspace` | `workspaces` | **ngay** (màn HOME) | `ui/workspace_tab.py::WorkspaceTab` |
|
||||
| 3 | `app.tab.monitoring` | `monitoring` | lười | `ui/monitoring_tab.py::MonitoringTab` |
|
||||
```text
|
||||
presentation/shell/main_window.py:151
|
||||
```
|
||||
|
||||
App mở lên là ở **Workspace ▸ Project**.
|
||||
Danh sách nằm trong `_nav_defs`.
|
||||
|
||||
## 2. Sub-tab của Workspace
|
||||
**Thứ tự trong bảng chính là page index.**
|
||||
|
||||
`ui/workspace_tab.py:214-245`:
|
||||
| Row | i18n key | Icon | Cách tạo | Widget |
|
||||
| --: | -------------------- | ------------ | --------------- | --------------------------------------------------------------- |
|
||||
| 0 | `app.tab.dashboard` | `dashboard` | Lazy | `presentation/dashboard/dashboard_tab.py::DashboardTab` |
|
||||
| 1 | `app.tab.schedule` | `schedule` | Lazy | `presentation/scheduling/schedule_task_tab.py::ScheduleTaskTab` |
|
||||
| 2 | `app.tab.workspace` | `workspaces` | Ngay khi mở app | `ui/workspace_tab.py::WorkspaceTab` |
|
||||
| 3 | `app.tab.monitoring` | `monitoring` | Lazy | `ui/monitoring_tab.py::MonitoringTab` |
|
||||
|
||||
| Tab | i18n key | Widget |
|
||||
|---|---|---|
|
||||
| Project | `workspace.tab_project` | `_build_project_tab()` trong chính file đó |
|
||||
| Cowork | `workspace.tab_cowork` | `ui/cowork_tab.py` |
|
||||
| Co4E | `workspace.tab_co4e` | `ui/co4e_tab.py` → `presentation/co4e/` |
|
||||
| Folder | `workspace.tab_folder` | `presentation/folder/folder_tab.py` |
|
||||
| GraphRAG | `workspace.tab_graphrag` | `presentation/graph/structure_graph_view.py` |
|
||||
### Màn hình mặc định
|
||||
|
||||
Monitoring **giữ tab strip riêng** với 8 sub-view (tổng quan, trạng thái agent, công cụ,
|
||||
nhật ký hành động, lịch sử gọi MCP, sự kiện bảo mật, agents admin, icon). Workspace là màn
|
||||
duy nhất giấu tab strip đi.
|
||||
Khi mở app, người dùng bắt đầu tại:
|
||||
|
||||
## 3. Thành phần luôn nổi trên mọi màn
|
||||
```text
|
||||
Workspace → Project
|
||||
```
|
||||
|
||||
| Thành phần | File | Triệu chứng người dùng hay mô tả |
|
||||
|---|---|---|
|
||||
| Nav rail trái, nút thu gọn | `presentation/shell/nav_rail.py` | "menu bị co lại", "không thấy tên project" |
|
||||
| Top bar (theme, ngôn ngữ) | `presentation/shell/top_bar.py` | "đổi giao diện không ăn" |
|
||||
| Toast góc trên trái | `presentation/shell/toast.py` | "thông báo xong việc che mất nút" |
|
||||
| Help agent nổi góc dưới phải | `ui/help_agent_widget.py` | "con robot che nút gửi" |
|
||||
| Status bar dưới cùng | `main_window.statusBar()` | "dòng chữ dưới đáy không đổi" |
|
||||
### Lưu ý về Lazy
|
||||
|
||||
## 4. Dialog
|
||||
`Dashboard`, `Schedule` và `Monitoring` được tạo **lazy** — chỉ được dựng khi người dùng mở màn hình.
|
||||
|
||||
`ui/`: `login_dialog.py`, `permission_dialog.py`, `settings_dialog.py`, `skills_dialog.py`,
|
||||
`task_editor_dialog.py`, `file_edit_dialog.py`, `flow_dialog.py`, `mcp_servers_dialog.py`,
|
||||
`co4e_agent_dialog.py`, `ext_connector_dialog.py`.
|
||||
Vì vậy, khi điều tra lỗi liên quan đến các màn hình này, phải kiểm tra cả **thời điểm widget được tạo** và **vòng đời của widget**.
|
||||
|
||||
## 5. 🔎 Hai file tra cứu bắt buộc dùng
|
||||
---
|
||||
|
||||
### `docs/screens/manifest.json`
|
||||
## 2. Các tab bên trong Workspace
|
||||
|
||||
Mỗi màn đã chụp ảnh có một entry: `slug`, `title`, `theme`, `note` (**đúng `file.py:line`
|
||||
nơi màn đó được dựng**), `file` (ảnh), `nav`.
|
||||
Các tab được định nghĩa trong:
|
||||
|
||||
```text
|
||||
ui/workspace_tab.py:214-245
|
||||
```
|
||||
|
||||
| Tab | i18n key | Widget/File |
|
||||
| -------- | ------------------------ | -------------------------------------------------- |
|
||||
| Project | `workspace.tab_project` | `_build_project_tab()` trong `ui/workspace_tab.py` |
|
||||
| Cowork | `workspace.tab_cowork` | `ui/cowork_tab.py` |
|
||||
| Co4E | `workspace.tab_co4e` | `ui/co4e_tab.py` → `presentation/co4e/` |
|
||||
| Folder | `workspace.tab_folder` | `presentation/folder/folder_tab.py` |
|
||||
| GraphRAG | `workspace.tab_graphrag` | `presentation/graph/structure_graph_view.py` |
|
||||
|
||||
### Monitoring có cấu trúc khác
|
||||
|
||||
Monitoring có **tab strip riêng**, gồm 8 sub-view:
|
||||
|
||||
1. Tổng quan.
|
||||
2. Trạng thái Agent.
|
||||
3. Công cụ.
|
||||
4. Nhật ký hành động.
|
||||
5. Lịch sử gọi MCP.
|
||||
6. Sự kiện bảo mật.
|
||||
7. Agents Admin.
|
||||
8. Icon.
|
||||
|
||||
**Workspace là màn hình duy nhất không hiển thị tab strip theo cách này.**
|
||||
|
||||
Nếu người dùng nói:
|
||||
|
||||
> "Tab trạng thái agent trong màn Monitoring"
|
||||
|
||||
thì không được nhầm nó với một tab của Workspace.
|
||||
|
||||
---
|
||||
|
||||
## 3. Các thành phần luôn xuất hiện trên mọi màn hình
|
||||
|
||||
Một số thành phần nằm ngoài nội dung của từng màn hình.
|
||||
|
||||
| Thành phần | File | Cách người dùng thường mô tả |
|
||||
| ------------------------------- | -------------------------------- | -------------------------------------------------- |
|
||||
| Nav rail bên trái / nút thu gọn | `presentation/shell/nav_rail.py` | "Menu bị co lại", "Không thấy tên project" |
|
||||
| Top bar / theme / ngôn ngữ | `presentation/shell/top_bar.py` | "Đổi giao diện không ăn", "Đổi ngôn ngữ không đổi" |
|
||||
| Toast góc trên trái | `presentation/shell/toast.py` | "Thông báo xong việc che mất nút" |
|
||||
| Help Agent góc dưới phải | `ui/help_agent_widget.py` | "Con robot che nút gửi" |
|
||||
| Status bar phía dưới | `main_window.statusBar()` | "Dòng chữ dưới đáy không đổi" |
|
||||
|
||||
### Quy tắc
|
||||
|
||||
Nếu người dùng mô tả một thành phần thuộc nhóm trên, **không cần tìm sub-tab trước**.
|
||||
|
||||
Hãy kiểm tra trực tiếp file tương ứng.
|
||||
|
||||
---
|
||||
|
||||
## 4. Các Dialog
|
||||
|
||||
Các dialog chính nằm trong `ui/`:
|
||||
|
||||
```text
|
||||
ui/
|
||||
├── login_dialog.py
|
||||
├── permission_dialog.py
|
||||
├── settings_dialog.py
|
||||
├── skills_dialog.py
|
||||
├── task_editor_dialog.py
|
||||
├── file_edit_dialog.py
|
||||
├── flow_dialog.py
|
||||
├── mcp_servers_dialog.py
|
||||
├── co4e_agent_dialog.py
|
||||
└── ext_connector_dialog.py
|
||||
```
|
||||
|
||||
Ví dụ:
|
||||
|
||||
> "Khi mở Permission thì nút Allow bị..."
|
||||
|
||||
→ kiểm tra trước:
|
||||
|
||||
```text
|
||||
ui/permission_dialog.py
|
||||
```
|
||||
|
||||
Không tự động tìm trong `presentation/` chỉ vì lỗi xảy ra trên UI.
|
||||
|
||||
---
|
||||
|
||||
# 5. Hai file tra cứu bắt buộc
|
||||
|
||||
Khi cần chuyển mô tả của người dùng thành `file:line`, phải ưu tiên sử dụng:
|
||||
|
||||
```text
|
||||
docs/screens/manifest.json
|
||||
docs/screens/controls.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5.1. `docs/screens/manifest.json`
|
||||
|
||||
File này chứa thông tin về các màn hình đã được chụp screenshot.
|
||||
|
||||
Mỗi màn hình có các thông tin chính:
|
||||
|
||||
```text
|
||||
slug
|
||||
title
|
||||
theme
|
||||
note
|
||||
file
|
||||
nav
|
||||
```
|
||||
|
||||
Trong đó:
|
||||
|
||||
* `slug` — tên định danh của màn hình.
|
||||
* `title` — tên hiển thị.
|
||||
* `theme` — Dark hoặc Light.
|
||||
* `note` — **vị trí code dựng màn hình (`file.py:line`)**.
|
||||
* `file` — đường dẫn đến screenshot.
|
||||
* `nav` — màn hình thuộc nav nào.
|
||||
|
||||
### Ví dụ
|
||||
|
||||
Người dùng nói:
|
||||
|
||||
> "Màn Kanban lịch trình bị lỗi."
|
||||
|
||||
Có thể tìm màn hình liên quan bằng:
|
||||
|
||||
```bash
|
||||
# Người dùng nói "màn Kanban lịch trình"
|
||||
python -c "import json;print([e for e in json.load(open('docs/screens/manifest.json')) if 'schedule' in e['slug']])"
|
||||
```
|
||||
|
||||
Ảnh có **cả bản dark và light** (`*-dark.png` / `*-light.png`) — dùng để đối chiếu trước/sau
|
||||
và để kiểm tra bug chỉ xảy ra ở một theme.
|
||||
Sau đó lấy `note` để biết:
|
||||
|
||||
### `docs/screens/controls.json`
|
||||
```text
|
||||
file.py:line
|
||||
```
|
||||
|
||||
Danh mục **mọi control** đã trích tự động từ source: `file`, `var`, `type` (`QLineEdit`...),
|
||||
`kind` (mô tả tiếng Việt: "ô nhập", "nút"...), `label`, `line`, `signals`, `object_name`.
|
||||
### Screenshot Dark và Light
|
||||
|
||||
Mỗi màn hình thường có hai ảnh:
|
||||
|
||||
```text
|
||||
<slug>-dark.png
|
||||
<slug>-light.png
|
||||
```
|
||||
|
||||
Dùng hai ảnh này để:
|
||||
|
||||
* So sánh trước/sau.
|
||||
* Kiểm tra lỗi chỉ xảy ra ở một theme.
|
||||
* Kiểm tra sự khác biệt giữa Dark Mode và Light Mode.
|
||||
|
||||
---
|
||||
|
||||
## 5.2. `docs/screens/controls.json`
|
||||
|
||||
Đây là danh sách các control được trích tự động từ source code.
|
||||
|
||||
Mỗi control có thông tin như:
|
||||
|
||||
```text
|
||||
file
|
||||
var
|
||||
type
|
||||
kind
|
||||
label
|
||||
line
|
||||
signals
|
||||
object_name
|
||||
```
|
||||
|
||||
Trong đó:
|
||||
|
||||
* `file` — file chứa control.
|
||||
* `var` — tên biến.
|
||||
* `type` — loại widget, ví dụ `QLineEdit`.
|
||||
* `kind` — mô tả dễ hiểu, ví dụ `"ô nhập"`, `"nút"`.
|
||||
* `label` — text/label liên quan.
|
||||
* `line` — dòng code.
|
||||
* `signals` — signal liên quan.
|
||||
* `object_name` — `objectName` của widget.
|
||||
|
||||
### Ví dụ
|
||||
|
||||
Người dùng nói:
|
||||
|
||||
> "Ô nhập email trong màn tài khoản bị lỗi."
|
||||
|
||||
Có thể tìm control bằng:
|
||||
|
||||
```bash
|
||||
# Người dùng nói "ô nhập email trong màn tài khoản"
|
||||
python - <<'PY'
|
||||
import json
|
||||
|
||||
for f in json.load(open('docs/screens/controls.json')):
|
||||
for c in f['controls']:
|
||||
if 'email' in (c['var'] + c['label']).lower():
|
||||
print(f["file"], c["line"], c["var"], c["type"], c["object_name"])
|
||||
text = (c['var'] + c['label']).lower()
|
||||
if 'email' in text:
|
||||
print(
|
||||
f["file"],
|
||||
c["line"],
|
||||
c["var"],
|
||||
c["type"],
|
||||
c["object_name"]
|
||||
)
|
||||
PY
|
||||
```
|
||||
|
||||
Cột `object_name` đặc biệt quan trọng khi sửa bug màu/style: rỗng nghĩa là widget **chưa**
|
||||
được style qua `_TEMPLATE`, nên nó đang ăn style mặc định của class — thường chính là
|
||||
nguyên nhân của "chỗ này nhìn khác chỗ kia".
|
||||
Từ kết quả có thể xác định:
|
||||
|
||||
## 6. Quy trình tra 4 bước cho Triage
|
||||
```text
|
||||
file
|
||||
line
|
||||
variable
|
||||
widget type
|
||||
objectName
|
||||
```
|
||||
|
||||
1. Xác định **nav row** (Dashboard / Schedule / Workspace / Monitoring) từ mô tả hoặc ảnh.
|
||||
2. Xác định **sub-tab / dialog**.
|
||||
3. Tra `manifest.json` → lấy `note` = `file.py:line`.
|
||||
4. Tra `controls.json` → lấy đúng `var` + `line` + `object_name` của control bị lỗi.
|
||||
---
|
||||
|
||||
Không qua đủ 4 bước thì `confidence` tối đa là `low`.
|
||||
## 6. `object_name` đặc biệt quan trọng khi điều tra UI
|
||||
|
||||
Khi sửa lỗi màu hoặc style, phải chú ý đến:
|
||||
|
||||
```text
|
||||
object_name
|
||||
```
|
||||
|
||||
Nếu `object_name` đang rỗng, có nghĩa widget đó **chưa được gắn `objectName` để áp style theo cơ chế template/QSS**.
|
||||
|
||||
Khi đó widget có thể đang sử dụng style mặc định của class.
|
||||
|
||||
Đây thường là nguyên nhân khiến người dùng thấy:
|
||||
|
||||
> "Chỗ này nhìn khác chỗ kia."
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
Widget A → objectName = "project_title"
|
||||
↓
|
||||
QSS áp style riêng
|
||||
|
||||
Widget B → objectName = ""
|
||||
↓
|
||||
dùng style mặc định
|
||||
```
|
||||
|
||||
Vì vậy, khi gặp lỗi visual liên quan đến màu/style, hãy kiểm tra `object_name` trước khi tự thêm màu hoặc `setStyleSheet()`.
|
||||
|
||||
---
|
||||
|
||||
# 7. Quy trình 4 bước dành cho Triage
|
||||
|
||||
Khi người dùng báo lỗi bằng ngôn ngữ tự nhiên, thực hiện theo thứ tự sau:
|
||||
|
||||
### Bước 1 — Xác định màn hình chính
|
||||
|
||||
Xác định lỗi thuộc:
|
||||
|
||||
```text
|
||||
Dashboard
|
||||
Schedule
|
||||
Workspace
|
||||
Monitoring
|
||||
```
|
||||
|
||||
Dựa trên mô tả của người dùng hoặc screenshot.
|
||||
|
||||
---
|
||||
|
||||
### Bước 2 — Xác định tab/view/dialog
|
||||
|
||||
Tiếp tục xác định:
|
||||
|
||||
```text
|
||||
Sub-tab
|
||||
→ View
|
||||
→ Dialog
|
||||
```
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
Workspace
|
||||
→ Co4E
|
||||
→ Agent Dialog
|
||||
```
|
||||
|
||||
hoặc:
|
||||
|
||||
```text
|
||||
Monitoring
|
||||
→ Security Events
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bước 3 — Tra `manifest.json`
|
||||
|
||||
Mở:
|
||||
|
||||
```text
|
||||
docs/screens/manifest.json
|
||||
```
|
||||
|
||||
Tìm màn hình tương ứng và lấy:
|
||||
|
||||
```text
|
||||
note → file.py:line
|
||||
```
|
||||
|
||||
Đây là điểm bắt đầu để tìm code dựng màn hình.
|
||||
|
||||
---
|
||||
|
||||
### Bước 4 — Tra `controls.json`
|
||||
|
||||
Nếu lỗi liên quan đến một control cụ thể, tiếp tục tìm trong:
|
||||
|
||||
```text
|
||||
docs/screens/controls.json
|
||||
```
|
||||
|
||||
Lấy:
|
||||
|
||||
```text
|
||||
var
|
||||
line
|
||||
type
|
||||
object_name
|
||||
```
|
||||
|
||||
Sau đó xác định chính xác widget bị lỗi.
|
||||
|
||||
---
|
||||
|
||||
# 8. Quy tắc về Confidence
|
||||
|
||||
Triage phải phản ánh đúng mức độ chắc chắn của kết quả.
|
||||
|
||||
Nếu chưa hoàn thành đủ 4 bước:
|
||||
|
||||
```text
|
||||
1. Nav
|
||||
2. Tab/View/Dialog
|
||||
3. manifest.json
|
||||
4. controls.json
|
||||
```
|
||||
|
||||
thì:
|
||||
|
||||
```yaml
|
||||
confidence: low
|
||||
```
|
||||
|
||||
Không được tự nâng lên `medium` hoặc `high` chỉ vì file nhìn có vẻ đúng.
|
||||
|
||||
### Khi nào có thể tăng Confidence?
|
||||
|
||||
Chỉ tăng khi có bằng chứng cụ thể, ví dụ:
|
||||
|
||||
```text
|
||||
User description
|
||||
↓
|
||||
Dashboard
|
||||
↓
|
||||
Statistics view
|
||||
↓
|
||||
manifest.json
|
||||
↓
|
||||
presentation/dashboard/dashboard_tab.py:123
|
||||
↓
|
||||
controls.json
|
||||
↓
|
||||
QTableView
|
||||
↓
|
||||
line 245
|
||||
```
|
||||
|
||||
Khi đó mới có đủ cơ sở để ghi nhận `file:line` và đánh giá confidence cao hơn.
|
||||
|
||||
---
|
||||
|
||||
# 9. Nguyên tắc quan trọng
|
||||
|
||||
**Không đoán file từ tên.**
|
||||
|
||||
Không nên suy luận kiểu:
|
||||
|
||||
> "Lỗi ở Workspace nên chắc chắn nằm trong `workspace_tab.py`."
|
||||
|
||||
Thay vào đó:
|
||||
|
||||
```text
|
||||
Mô tả của user
|
||||
↓
|
||||
Xác định màn hình
|
||||
↓
|
||||
Xác định tab/view/dialog
|
||||
↓
|
||||
Tra manifest.json
|
||||
↓
|
||||
Xác định file:line
|
||||
↓
|
||||
Tra controls.json
|
||||
↓
|
||||
Xác định widget/control
|
||||
↓
|
||||
Đánh giá confidence
|
||||
```
|
||||
|
||||
Mục tiêu cuối cùng của Screen Map là biến một mô tả mơ hồ của người dùng thành một đầu vào có thể sử dụng được cho `defect_record`, đặc biệt là:
|
||||
|
||||
```text
|
||||
screen
|
||||
widget
|
||||
file
|
||||
line
|
||||
object_name
|
||||
confidence
|
||||
```
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+635
-71
@@ -1,101 +1,665 @@
|
||||
# Theme & Design Tokens — luật màu sắc của Cowork Local
|
||||
# Theme & Design Tokens — Luật màu sắc của Cowork Local
|
||||
|
||||
Nguồn: docstring đầu `theme/__init__.py`, `theme/palettes.py`, `theme/qss.py`,
|
||||
`theme/qss_controls.py`.
|
||||
> Knowledge module dành cho các agent xử lý **UI Visual / Theme / QSS** của Cowork Local.
|
||||
|
||||
## Nguồn chính
|
||||
|
||||
* `theme/__init__.py` — docstring và API theme
|
||||
* `theme/palettes.py` — định nghĩa Palette/token
|
||||
* `theme/qss.py` — `_TEMPLATE` và stylesheet
|
||||
* `theme/qss_controls.py` — style cho các Qt controls
|
||||
|
||||
---
|
||||
|
||||
## 1. Luật gốc
|
||||
# 1. Luật quan trọng nhất
|
||||
|
||||
> **Không file nào ngoài `theme/` được đặt tên một màu.**
|
||||
> **Ngoài thư mục `theme/`, không file nào được tự định nghĩa màu.**
|
||||
|
||||
Cơ chế duy nhất:
|
||||
Luồng màu chuẩn của Cowork Local:
|
||||
|
||||
```text
|
||||
Palette (token ngữ nghĩa) → _TEMPLATE (một QSS duy nhất) → stylesheet(theme)
|
||||
Palette
|
||||
↓
|
||||
token ngữ nghĩa
|
||||
↓
|
||||
_TEMPLATE
|
||||
↓
|
||||
stylesheet(theme)
|
||||
↓
|
||||
QApplication.setStyleSheet(...)
|
||||
```
|
||||
|
||||
Hai cách hợp lệ để một widget có màu:
|
||||
Nói đơn giản:
|
||||
|
||||
1. **Khai báo** — gán `objectName` cho widget, style nó trong `_TEMPLATE`
|
||||
(`theme/qss.py`). Đây là cách mặc định.
|
||||
2. **Vẽ tay** — widget vẽ bằng `QPainter` (chart, canvas, syntax highlighter) thì gọi
|
||||
`current_palette()` rồi đọc token.
|
||||
> **Widget không tự chọn màu. Theme quyết định màu.**
|
||||
|
||||
Cách **không** hợp lệ, bị reject review:
|
||||
---
|
||||
|
||||
# 2. Hai cách hợp lệ để widget có màu
|
||||
|
||||
## Cách 1 — Style bằng QSS
|
||||
|
||||
Đây là cách mặc định.
|
||||
|
||||
Widget đặt `objectName`, sau đó style được định nghĩa trong:
|
||||
|
||||
```text
|
||||
theme/qss.py
|
||||
```
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```python
|
||||
self.label.setStyleSheet("color: #dc2626;") # ❌ hex ngoài theme/
|
||||
pen.setColor(QColor("red")) # ❌ tên màu literal
|
||||
self.card.setStyleSheet("background: rgba(0,0,0,.1)") # ❌
|
||||
widget.setObjectName("my_widget")
|
||||
```
|
||||
|
||||
## 2. API cần nhớ
|
||||
và style tương ứng nằm trong `_TEMPLATE`.
|
||||
|
||||
| Hàm | Dùng khi |
|
||||
|---|---|
|
||||
| `theme.stylesheet(theme)` | Sinh QSS toàn app, truyền vào `QApplication.setStyleSheet` |
|
||||
| `theme.set_active_theme(theme)` | **Phải** gọi ngay cạnh mỗi `setStyleSheet(stylesheet(...))` |
|
||||
| `theme.current_theme()` | `'dark'` / `'light'` đang hiển thị |
|
||||
| `theme.current_palette()` | Token của theme đang hiển thị — dùng trong `paintEvent` |
|
||||
| `theme.palette(theme)` | Token của một theme cụ thể |
|
||||
| `theme.resolve_theme('system')` | Suy ra dark/light từ color scheme của OS |
|
||||
| `theme.role_colors(theme)` | Màu theo vai trò hội thoại: user/assistant/tool/result/error |
|
||||
---
|
||||
|
||||
`current_palette()` tồn tại để code vẽ **không** phải đọc lại `config.json` mỗi lần
|
||||
repaint — đó từng là bug hiệu năng thật. Không thay bằng đọc config.
|
||||
## Cách 2 — Widget tự vẽ bằng `QPainter`
|
||||
|
||||
## 3. Nhóm token
|
||||
Dùng cho các thành phần như:
|
||||
|
||||
Palette là `@dataclass(frozen=True)`. Các nhóm chính:
|
||||
* chart;
|
||||
* canvas;
|
||||
* syntax highlighter;
|
||||
* custom painting.
|
||||
|
||||
| Nhóm | Token | Ý nghĩa |
|
||||
|---|---|---|
|
||||
| Bề mặt (thang 4 bậc) | `bg` | nền cửa sổ / canvas |
|
||||
| | `surface` | panel, card, group box (**không** phải nav rail) |
|
||||
| | `surface_raised` | input, list, tree — thứ người dùng gõ/chọn |
|
||||
| | `overlay` | menu, tooltip, popup |
|
||||
| | `sunken` | log, code, terminal — thứ để đọc vào |
|
||||
| | `hover` / `active` | trạng thái hover / đang bấm |
|
||||
| Chữ | `text`, `text_muted`, ... | |
|
||||
| Nhấn | `accent`, `accent_solid` | **Hai token khác nhau có chủ đích**: màu đọc được *dạng chữ* trên nền tối thì quá nhạt để làm *nền* cho chữ trắng |
|
||||
| Trạng thái | `danger`, ... | |
|
||||
| Vai trò hội thoại | `role_user`, `role_assistant`, `role_tool`, `role_result`, `role_error` | |
|
||||
| Code | `code_string`, ... | syntax highlighting |
|
||||
Code phải lấy màu từ:
|
||||
|
||||
Token là **ngữ nghĩa**, không phải literal: `danger` / `text_muted` — không bao giờ
|
||||
`blue` / `grey2`. Thêm một theme = thêm một `Palette`, không phải sửa stylesheet.
|
||||
```python
|
||||
current_palette()
|
||||
```
|
||||
|
||||
## 4. Ràng buộc thiết kế (đừng "sửa" nhầm thành bug)
|
||||
Ví dụ:
|
||||
|
||||
- **Không gradient, không glow.** Bảng màu lấy từ VS Code "Dark Modern" / "Light Modern".
|
||||
Bề mặt phẳng, góc gần vuông, một màu accent chỉ dành cho thứ người dùng thao tác.
|
||||
- **Chiều sâu đến từ thang bề mặt và viền mảnh**, không từ màu.
|
||||
- **Silhouette VS Code:** nav rail **tối hơn** vùng nội dung, không sáng hơn.
|
||||
Người dùng báo "menu trái tối quá" — đó là thiết kế, không phải bug. Xem `examples/bad_fix.md`.
|
||||
- **Contrast giữ ở WCAG AA (4.5:1)** cho body text và cho chữ trên nút đặc.
|
||||
- Bốn giá trị của VS Code không đạt AA đã được nhích lên vừa đủ (số dòng dark 3.59:1,
|
||||
chữ mờ trên sidebar sáng 4.28:1, xanh lá sáng 4.33:1, hổ phách sáng 3.12:1). Mỗi chỗ có
|
||||
comment ghi giá trị gốc — **không** trả chúng về giá trị VS Code.
|
||||
```python
|
||||
palette = current_palette()
|
||||
```
|
||||
|
||||
## 5. Mũi tên combo box (`_chevron_asset`)
|
||||
Sau đó dùng token từ palette.
|
||||
|
||||
QSS `image:` chỉ nhận đường dẫn file/resource, không nhận `QPixmap`. Và một khi
|
||||
`::drop-down` / `::up-button` / `::down-button` bị style, Qt **ngừng vẽ mũi tên mặc định**.
|
||||
Vì vậy `theme/palettes.py::_chevron_asset` render sẵn PNG chevron ra thư mục tạm và cache
|
||||
theo hash `(direction, color)`.
|
||||
---
|
||||
|
||||
Hệ quả khi debug:
|
||||
# 3. Những cách KHÔNG được phép
|
||||
|
||||
- "Combo box mất mũi tên" → gần như luôn do một stylesheet cục bộ đè lên `::drop-down`.
|
||||
- File cache nằm ở `%TEMP%/cowork_local_theme/chevron_*.png`. Xoá nó để buộc render lại
|
||||
khi test màu mới.
|
||||
Không được tự đặt màu trong UI code.
|
||||
|
||||
## 6. Checklist sửa bug liên quan màu sắc
|
||||
### ❌ Hardcode HEX
|
||||
|
||||
- [ ] Đã kiểm tra bug xuất hiện ở **cả** dark và light chưa? (`docs/screens/*-dark.png` / `*-light.png`)
|
||||
- [ ] Bản sửa dùng token, không dùng hex?
|
||||
- [ ] Nếu thêm token mới: đã thêm cho **cả** `DARK` và `LIGHT`?
|
||||
- [ ] Nếu là chữ trên nền đặc: đã dùng `accent_solid` thay vì `accent`?
|
||||
- [ ] Contrast còn ≥ 4.5:1?
|
||||
- [ ] Widget dựng sau khi đổi theme có nhận đúng stylesheet? (xem `qt_pitfalls.md` P07)
|
||||
```python
|
||||
self.label.setStyleSheet("color: #dc2626;")
|
||||
```
|
||||
|
||||
### ❌ Hardcode tên màu
|
||||
|
||||
```python
|
||||
pen.setColor(QColor("red"))
|
||||
```
|
||||
|
||||
### ❌ Hardcode RGBA
|
||||
|
||||
```python
|
||||
self.card.setStyleSheet(
|
||||
"background: rgba(0,0,0,.1)"
|
||||
)
|
||||
```
|
||||
|
||||
Các trường hợp này phải bị reject khi review.
|
||||
|
||||
### Rule ngắn gọn
|
||||
|
||||
```text
|
||||
Không có màu literal ngoài theme/
|
||||
```
|
||||
|
||||
Không chỉ tránh `#hex`, mà cả:
|
||||
|
||||
* tên màu;
|
||||
* RGB;
|
||||
* RGBA;
|
||||
* stylesheet cục bộ chứa màu.
|
||||
|
||||
---
|
||||
|
||||
# 4. API Theme cần nhớ
|
||||
|
||||
| API | Dùng để |
|
||||
| ------------------------------- | --------------------------------------------------- |
|
||||
| `theme.stylesheet(theme)` | Tạo QSS cho toàn app |
|
||||
| `theme.set_active_theme(theme)` | Ghi nhận theme hiện đang active |
|
||||
| `theme.current_theme()` | Lấy theme hiện tại: `dark` / `light` |
|
||||
| `theme.current_palette()` | Lấy Palette của theme hiện tại |
|
||||
| `theme.palette(theme)` | Lấy Palette của một theme cụ thể |
|
||||
| `theme.resolve_theme("system")` | Xác định dark/light theo OS |
|
||||
| `theme.role_colors(theme)` | Lấy màu theo role: user/assistant/tool/result/error |
|
||||
|
||||
---
|
||||
|
||||
## Khi đổi theme
|
||||
|
||||
Hai lệnh này phải đi cùng nhau:
|
||||
|
||||
```python
|
||||
theme.set_active_theme(theme)
|
||||
app.setStyleSheet(theme.stylesheet(theme))
|
||||
```
|
||||
|
||||
Không được chỉ gọi `setStyleSheet()` mà quên cập nhật active theme.
|
||||
|
||||
---
|
||||
|
||||
# 5. `current_palette()` dùng để làm gì?
|
||||
|
||||
Code vẽ bằng `QPainter` phải dùng:
|
||||
|
||||
```python
|
||||
current_palette()
|
||||
```
|
||||
|
||||
Không được mỗi lần `paintEvent()` lại đọc:
|
||||
|
||||
```text
|
||||
config.json
|
||||
```
|
||||
|
||||
Lý do:
|
||||
|
||||
```text
|
||||
paintEvent()
|
||||
↓
|
||||
repaint
|
||||
↓
|
||||
đọc config
|
||||
↓
|
||||
lặp lại rất nhiều lần
|
||||
```
|
||||
|
||||
Điều này từng gây vấn đề hiệu năng thực tế.
|
||||
|
||||
Vì vậy:
|
||||
|
||||
> `current_palette()` tồn tại để custom painting lấy màu nhanh từ theme hiện tại.
|
||||
|
||||
---
|
||||
|
||||
# 6. Palette và Design Token
|
||||
|
||||
`Palette` là:
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
```
|
||||
|
||||
Token phải mang **ý nghĩa**, không phải tên màu.
|
||||
|
||||
### ❌ Không đặt token kiểu:
|
||||
|
||||
```text
|
||||
blue
|
||||
grey2
|
||||
dark_blue
|
||||
light_grey
|
||||
```
|
||||
|
||||
### ✅ Đặt theo vai trò:
|
||||
|
||||
```text
|
||||
accent
|
||||
danger
|
||||
text
|
||||
text_muted
|
||||
surface
|
||||
surface_raised
|
||||
```
|
||||
|
||||
Lợi ích:
|
||||
|
||||
> Thêm theme mới = thêm một `Palette`, không phải viết lại stylesheet.
|
||||
|
||||
---
|
||||
|
||||
# 7. Các nhóm token chính
|
||||
|
||||
## 7.1. Surface — các mức bề mặt
|
||||
|
||||
| Token | Dùng cho |
|
||||
| ---------------- | -------------------------------------------- |
|
||||
| `bg` | Nền chính của cửa sổ/canvas |
|
||||
| `surface` | Panel, card, group box |
|
||||
| `surface_raised` | Input, list, tree — nơi người dùng nhập/chọn |
|
||||
| `overlay` | Menu, tooltip, popup |
|
||||
| `sunken` | Log, code, terminal — vùng chủ yếu để đọc |
|
||||
| `hover` | Trạng thái hover |
|
||||
| `active` | Trạng thái đang active/pressed |
|
||||
|
||||
### Lưu ý
|
||||
|
||||
`surface` **không có nghĩa là nav rail**.
|
||||
|
||||
Nav rail có chủ đích riêng về độ sáng/tối.
|
||||
|
||||
---
|
||||
|
||||
## 7.2. Text
|
||||
|
||||
Các token chính:
|
||||
|
||||
```text
|
||||
text
|
||||
text_muted
|
||||
...
|
||||
```
|
||||
|
||||
Dùng token theo vai trò thay vì tự chọn màu.
|
||||
|
||||
---
|
||||
|
||||
## 7.3. Accent
|
||||
|
||||
Có hai token:
|
||||
|
||||
```text
|
||||
accent
|
||||
accent_solid
|
||||
```
|
||||
|
||||
**Hai token này khác nhau có chủ đích.**
|
||||
|
||||
### `accent`
|
||||
|
||||
Dùng cho accent thông thường, ví dụ:
|
||||
|
||||
* trạng thái;
|
||||
* thành phần UI;
|
||||
* điểm nhấn.
|
||||
|
||||
### `accent_solid`
|
||||
|
||||
Dùng khi accent trở thành **nền đặc và bên trên có chữ**.
|
||||
|
||||
Lý do:
|
||||
|
||||
> Một màu accent có thể đủ sáng để đọc khi dùng như chữ trên nền tối, nhưng lại quá sáng khi dùng làm nền cho chữ trắng.
|
||||
|
||||
Vì vậy:
|
||||
|
||||
```text
|
||||
Chữ trên nền accent đặc
|
||||
↓
|
||||
accent_solid
|
||||
```
|
||||
|
||||
Không tự lấy `accent` chỉ vì nó có vẻ "cùng màu".
|
||||
|
||||
---
|
||||
|
||||
## 7.4. State
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
danger
|
||||
...
|
||||
```
|
||||
|
||||
Các state token cũng phải mang ý nghĩa, không đặt theo tên màu.
|
||||
|
||||
---
|
||||
|
||||
## 7.5. Conversation roles
|
||||
|
||||
Có các token:
|
||||
|
||||
```text
|
||||
role_user
|
||||
role_assistant
|
||||
role_tool
|
||||
role_result
|
||||
role_error
|
||||
```
|
||||
|
||||
Dùng để phân biệt các role trong giao diện hội thoại.
|
||||
|
||||
---
|
||||
|
||||
## 7.6. Code / Syntax
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
code_string
|
||||
...
|
||||
```
|
||||
|
||||
Dùng cho syntax highlighting.
|
||||
|
||||
---
|
||||
|
||||
# 8. Các nguyên tắc thiết kế — đừng nhầm thành bug
|
||||
|
||||
Một số đặc điểm nhìn "khác mắt" nhưng **có chủ đích**.
|
||||
|
||||
Không được tự ý sửa chỉ vì người dùng nói "trông hơi tối" hoặc "không giống app hiện đại".
|
||||
|
||||
---
|
||||
|
||||
## 8.1. Không gradient, không glow
|
||||
|
||||
Thiết kế lấy cảm hứng từ:
|
||||
|
||||
```text
|
||||
VS Code Dark Modern
|
||||
VS Code Light Modern
|
||||
```
|
||||
|
||||
Phong cách chính:
|
||||
|
||||
* surface phẳng;
|
||||
* góc gần vuông;
|
||||
* không gradient;
|
||||
* không glow;
|
||||
* một accent chính;
|
||||
* accent dành cho thứ người dùng tương tác.
|
||||
|
||||
---
|
||||
|
||||
## 8.2. Độ sâu đến từ surface và border
|
||||
|
||||
Không tạo chiều sâu bằng cách:
|
||||
|
||||
```text
|
||||
đổi màu quá mạnh
|
||||
```
|
||||
|
||||
Thay vào đó dùng:
|
||||
|
||||
```text
|
||||
surface hierarchy
|
||||
+
|
||||
border mảnh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 9. Nav rail tối hơn là thiết kế có chủ đích
|
||||
|
||||
Silhouette của Cowork Local lấy theo VS Code:
|
||||
|
||||
```text
|
||||
NAV RAIL
|
||||
↓
|
||||
tối hơn
|
||||
↓
|
||||
CONTENT AREA
|
||||
```
|
||||
|
||||
Không phải:
|
||||
|
||||
```text
|
||||
nav rail sáng hơn content
|
||||
```
|
||||
|
||||
Vì vậy nếu user báo:
|
||||
|
||||
> "Menu bên trái tối quá."
|
||||
|
||||
thì **chưa được kết luận ngay là visual bug**.
|
||||
|
||||
Đây có thể là design intent.
|
||||
|
||||
Xem thêm:
|
||||
|
||||
```text
|
||||
examples/bad_fix.md
|
||||
```
|
||||
|
||||
để tránh sửa nhầm.
|
||||
|
||||
---
|
||||
|
||||
# 10. Contrast — WCAG AA
|
||||
|
||||
Body text và chữ trên button nền đặc phải đạt:
|
||||
|
||||
```text
|
||||
Contrast ratio ≥ 4.5:1
|
||||
```
|
||||
|
||||
Đây là yêu cầu tối thiểu.
|
||||
|
||||
Khi thay token/màu:
|
||||
|
||||
```text
|
||||
Dark theme
|
||||
+
|
||||
Light theme
|
||||
+
|
||||
text/background
|
||||
```
|
||||
|
||||
đều phải được kiểm tra.
|
||||
|
||||
---
|
||||
|
||||
## Không khôi phục màu VS Code cũ nếu màu đó không đạt AA
|
||||
|
||||
Một số màu gốc của VS Code không đạt yêu cầu AA.
|
||||
|
||||
Các giá trị đã được Cowork Local điều chỉnh vừa đủ, ví dụ:
|
||||
|
||||
| Trường hợp | Contrast cũ |
|
||||
| ------------------------ | ----------: |
|
||||
| Dark line | 3.59:1 |
|
||||
| Chữ mờ trên sidebar sáng | 4.28:1 |
|
||||
| Xanh lá sáng | 4.33:1 |
|
||||
| Hổ phách sáng | 3.12:1 |
|
||||
|
||||
Các chỗ này có comment ghi lại giá trị gốc.
|
||||
|
||||
### Rule
|
||||
|
||||
**Không đưa chúng trở lại giá trị VS Code ban đầu.**
|
||||
|
||||
Mục tiêu của Cowork Local là:
|
||||
|
||||
```text
|
||||
VS Code silhouette
|
||||
+
|
||||
WCAG AA
|
||||
```
|
||||
|
||||
không phải copy nguyên xi mọi giá trị màu của VS Code.
|
||||
|
||||
---
|
||||
|
||||
# 11. ⚠️ Combo Box và `_chevron_asset`
|
||||
|
||||
Một lỗi dễ gặp:
|
||||
|
||||
> Combo box mất mũi tên.
|
||||
|
||||
Nguyên nhân liên quan đến cách Qt xử lý QSS.
|
||||
|
||||
---
|
||||
|
||||
## 11.1. `image:` trong QSS không nhận `QPixmap`
|
||||
|
||||
QSS:
|
||||
|
||||
```text
|
||||
image:
|
||||
```
|
||||
|
||||
chỉ nhận đường dẫn tới:
|
||||
|
||||
* file;
|
||||
* resource.
|
||||
|
||||
Không nhận trực tiếp:
|
||||
|
||||
```text
|
||||
QPixmap
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11.2. Style `::drop-down` sẽ làm Qt ngừng vẽ arrow mặc định
|
||||
|
||||
Khi style các selector như:
|
||||
|
||||
```text
|
||||
::drop-down
|
||||
::up-button
|
||||
::down-button
|
||||
```
|
||||
|
||||
Qt có thể ngừng vẽ mũi tên mặc định.
|
||||
|
||||
---
|
||||
|
||||
## 11.3. Cowork Local dùng `_chevron_asset`
|
||||
|
||||
Trong:
|
||||
|
||||
```text
|
||||
theme/palettes.py
|
||||
```
|
||||
|
||||
`_chevron_asset`:
|
||||
|
||||
1. render chevron thành PNG;
|
||||
2. lưu vào thư mục tạm;
|
||||
3. cache theo:
|
||||
|
||||
```text
|
||||
(direction, color)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Khi debug combo box
|
||||
|
||||
Nếu thấy:
|
||||
|
||||
> Combo box mất mũi tên.
|
||||
|
||||
Hãy kiểm tra trước:
|
||||
|
||||
```text
|
||||
stylesheet cục bộ
|
||||
↓
|
||||
::drop-down
|
||||
```
|
||||
|
||||
Đây thường là nguyên nhân.
|
||||
|
||||
Cache nằm tại:
|
||||
|
||||
```text
|
||||
%TEMP%/cowork_local_theme/chevron_*.png
|
||||
```
|
||||
|
||||
Nếu đang test màu mới, có thể xóa cache để buộc render lại.
|
||||
|
||||
---
|
||||
|
||||
# 12. Checklist sửa bug màu sắc/theme
|
||||
|
||||
Trước khi hoàn thành visual fix, kiểm tra:
|
||||
|
||||
### Theme coverage
|
||||
|
||||
* [ ] Bug đã được kiểm tra trên **Dark** chưa?
|
||||
* [ ] Bug đã được kiểm tra trên **Light** chưa?
|
||||
* [ ] Có thể dùng screenshot:
|
||||
|
||||
* `docs/screens/*-dark.png`
|
||||
* `docs/screens/*-light.png`
|
||||
|
||||
### Token
|
||||
|
||||
* [ ] Patch dùng semantic token thay vì hex literal?
|
||||
* [ ] Không có `setStyleSheet()` cục bộ để thay màu?
|
||||
* [ ] Không có `QColor("red")`, `QColor("blue")`, v.v.?
|
||||
* [ ] Nếu thêm token mới, đã thêm cho **cả `DARK` và `LIGHT`**?
|
||||
* [ ] Token mới có tên theo **ý nghĩa**, không theo màu?
|
||||
|
||||
### Accent
|
||||
|
||||
* [ ] Chữ trên nền accent đặc đã dùng `accent_solid`?
|
||||
* [ ] Không dùng `accent` chỉ vì hai token có vẻ giống nhau?
|
||||
|
||||
### Accessibility
|
||||
|
||||
* [ ] Contrast đạt **≥ 4.5:1**?
|
||||
* [ ] Đã kiểm tra cả text và button có nền đặc?
|
||||
|
||||
### Theme lifecycle
|
||||
|
||||
* [ ] Widget tạo sau khi đổi theme có nhận đúng stylesheet?
|
||||
* [ ] Đã kiểm tra vấn đề lazy screen theo `qt_pitfalls.md` **P07**?
|
||||
|
||||
### Design intent
|
||||
|
||||
* [ ] Không vô tình thêm gradient?
|
||||
* [ ] Không thêm glow?
|
||||
* [ ] Không làm nav rail sáng hơn content?
|
||||
* [ ] Không khôi phục các màu VS Code cũ đã bị loại vì không đạt WCAG AA?
|
||||
|
||||
---
|
||||
|
||||
# 13. Quy tắc review nhanh
|
||||
|
||||
Khi gặp một defect liên quan màu sắc, đi theo thứ tự:
|
||||
|
||||
```text
|
||||
1. Xác định widget
|
||||
↓
|
||||
2. Kiểm tra objectName
|
||||
↓
|
||||
3. Tìm rule trong theme/qss.py
|
||||
↓
|
||||
4. Kiểm tra token trong palettes.py
|
||||
↓
|
||||
5. Kiểm tra DARK + LIGHT
|
||||
↓
|
||||
6. Kiểm tra contrast
|
||||
↓
|
||||
7. Kiểm tra local setStyleSheet()
|
||||
↓
|
||||
8. Kiểm tra lazy theme lifecycle (P07)
|
||||
↓
|
||||
9. Xác định đây là bug thật hay design intent
|
||||
↓
|
||||
10. Chỉ sau đó mới tạo fix_plan
|
||||
```
|
||||
|
||||
## Nguyên tắc cuối
|
||||
|
||||
```text
|
||||
UI code
|
||||
↓
|
||||
không tự chọn màu
|
||||
↓
|
||||
semantic token
|
||||
↓
|
||||
Palette
|
||||
↓
|
||||
_TEMPLATE / current_palette()
|
||||
↓
|
||||
theme
|
||||
```
|
||||
|
||||
**Nếu một màu mới cần xuất hiện, trước tiên hỏi:**
|
||||
|
||||
> "Màu này đang đại diện cho vai trò gì?"
|
||||
|
||||
Sau đó tạo hoặc dùng **semantic token** phù hợp.
|
||||
|
||||
Không hỏi:
|
||||
|
||||
> "Mình muốn màu xanh nào?"
|
||||
|
||||
Vì trong Cowork Local, **ý nghĩa của màu quan trọng hơn bản thân màu**.
|
||||
|
||||
Reference in New Issue
Block a user