Files
cowork-local/ui/co4e_agent_dialog.py
d2101e1f87
CI / test (pull_request) Canceled after 0s
fix(i18n): dịch nốt chữ do Qt tự vẽ, và thêm dòng phiên bản ở góc phải
Người dùng báo: chọn tiếng Nhật mà nhóm Sandbox Security, nút Save/Cancel và
nhiều chỗ khác vẫn tiếng Anh. Bộ test i18n cũ vẫn xanh vì nó chỉ bắt lỗi "có
dịch nhưng không ai áp lại" — hai lỗ thật nằm chỗ khác:

* Chuỗi HARDCODE không đi qua ``tr()`` bao giờ (``QPushButton("Unlock")``), nên
  phép đo "đổi ngôn ngữ rồi tìm chỗ không mang mốc" thấy nó đứng yên ở cả hai
  lần chụp và coi là bình thường.
* Nhãn nút do CHÍNH Qt vẽ. ``QDialogButtonBox``, ``QMessageBox.question`` và
  ``QInputDialog.get*`` lấy chữ từ bảng dịch riêng của Qt; ứng dụng không cài
  ``QTranslator`` nào và bản PySide6 đang dùng cũng không đóng gói file
  ``qtbase_*.qm`` nào để cài — nên chúng luôn rơi về tiếng Anh.

``ui/dialog_buttons.py`` gán nhãn của dự án đè lên nhãn Qt: ``dialog_buttons``
(10 hộp thoại), ``confirm`` (13 hộp Có/Không), ``ask_text``/``ask_multiline``/
``ask_item`` (16 hộp nhập liệu). Cùng với 17 chuỗi hardcode và 5 câu lỗi mà
``core/tasks.py`` trả thẳng ra hộp thoại — nay trả KHOÁ i18n, nơi hiển thị mới
gọi ``tr()`` — là 61 chỗ.

Ba chỗ nữa cùng lớp lỗi, phát hiện khi rà lại:

* ``_add_section`` nhận chuỗi ĐÃ dịch nên bốn tiêu đề mục của Step config đứng
  nguyên ở ngôn ngữ lúc dựng panel. Nay nhận khoá + ``bind_dynamic`` để không
  mất trạng thái gập/mở khi đổi ngôn ngữ.
* Thẻ tool ở Giám sát ▸ Công cụ hiện thẳng ``spec.description`` — chuỗi gửi cho
  MÔ HÌNH trong schema function-calling, phải giữ tiếng Anh. Thêm bộ mô tả hiển
  thị riêng cho 9 tool.
* Tên nhóm catalog ở tab Connector ("Other (any generic MCP server)").

Kèm theo, phần giao diện người dùng yêu cầu:

* ``__version__`` 2.26.0 -> 0.0.1, một nguồn cho tiêu đề cửa sổ, tab Giới thiệu
  và dòng mới ở góc phải thanh trạng thái (thay dòng ghi công tác giả).
* Tắt size grip: nó vẽ một vệt ngay bên phải dòng phiên bản. Cửa sổ vẫn kéo
  giãn được từ các cạnh.
* ``_NAV_SETTINGS_GAP`` 10 -> 4: hàng Cài đặt bớt xa nhóm Dashboard/Giám sát.

Hai test SẼ TREO nếu không sửa kèm: chúng patch ``QInputDialog.getText/getItem``
để tự trả lời, mà code nay gọi ``ask_text``/``ask_item`` — patch không còn chặn
được và hộp thoại thật sẽ mở ra chờ người bấm.

Ba cổng mới trong ``tests/ui/test_i18n_khong_hardcode_chu.py`` canh ở mức cấu
trúc (không ai được dựng lại kiểu cũ); đã kiểm chúng CẮN trên bản trước khi sửa:
10 + 13 + 17 vi phạm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 01:43:22 +09:00

236 lines
10 KiB
Python

"""Co4E custom-agent editor — create/edit a persisted persona.
Mirrors nova's agent-config-panel field set: name, role, icon, instructions,
model, permission preset, and an attached-skills checklist.
"""
from __future__ import annotations
from typing import List
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QComboBox, QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout,
QLineEdit, QListWidget, QListWidgetItem, QPlainTextEdit, QPushButton,
QVBoxLayout, QWidget,
)
from ..core.co4e import PERMISSION_PRESETS, CustomAgent
from ..core.worker import AgentWorker
from ..i18n import tr
from .dialog_buttons import ask_multiline, dialog_buttons
from .icons import icon, icon_picker_combo
class Co4EAgentDialog(QDialog):
"""Hộp thoại tạo/sửa một agent tự tạo của Co4E: tên, vai trò, model, chỉ dẫn,
skill và tệp đính kèm.
"""
def __init__(self, ctx, agent: CustomAgent, skill_names: List[str], parent=None):
"""Form thêm/sửa một agent Co4E: tên, prompt và các skill nó được dùng."""
super().__init__(parent)
self.ctx = ctx
self._agent = agent
self.setWindowTitle(tr("co4e.agent_edit_title") if agent.name else tr("co4e.agent_new_title"))
self.resize(460, 480)
form = QFormLayout(self)
self.name_edit = QLineEdit(agent.name)
form.addRow(tr("co4e.f_name"), self.name_edit)
self.role_edit = QLineEdit(agent.role or "AGENT")
form.addRow(tr("co4e.f_role"), self.role_edit)
# Dropdown of every icon in the registry (Monitoring's Icon Management
# set + built-ins), each row previewing its actual glyph — still
# editable so a not-yet-added custom name can be typed directly.
self.icon_edit = icon_picker_combo(agent.icon)
self.icon_edit.lineEdit().setPlaceholderText(tr("co4e.f_icon_placeholder"))
form.addRow(tr("co4e.f_icon"), self.icon_edit)
self.instructions_edit = QPlainTextEdit(agent.instructions)
self.instructions_edit.setMaximumHeight(150)
# ✨ AI-assist — draft the agent's instructions from its name + role
# (handy when you're not attaching a skill).
self.gen_btn = QPushButton(tr("co4e.ai_draft"))
self.gen_btn.setIcon(icon("sparkle"))
self.gen_btn.setToolTip(tr("co4e.ai_draft_tooltip"))
self.gen_btn.setEnabled(ctx is not None)
self.gen_btn.clicked.connect(self._ai_draft)
instr_box = QWidget()
ib = QVBoxLayout(instr_box)
ib.setContentsMargins(0, 0, 0, 0)
ib.addWidget(self.instructions_edit)
ib.addWidget(self.gen_btn, alignment=Qt.AlignRight)
form.addRow(tr("co4e.f_instructions"), instr_box)
# Extra context — free-text background/info fed to the agent at run time.
self.context_edit = QPlainTextEdit(getattr(agent, "context", ""))
self.context_edit.setMaximumHeight(90)
self.context_edit.setPlaceholderText(tr("co4e.f_context_placeholder"))
form.addRow(tr("co4e.f_context"), self.context_edit)
model_row = QHBoxLayout()
self.model_combo = QComboBox()
self.model_combo.setEditable(True)
self.model_combo.setEditText(agent.model)
self.load_btn = QPushButton()
self.load_btn.setIcon(icon("download"))
self.load_btn.setToolTip(tr("co4e.load_models_tooltip"))
self.load_btn.clicked.connect(self._load_models)
self.load_btn.setEnabled(ctx is not None)
model_row.addWidget(self.model_combo, 1)
model_row.addWidget(self.load_btn)
mrow = QWidget(); mrow.setLayout(model_row)
form.addRow(tr("co4e.f_model"), mrow)
self.perm_combo = QComboBox()
for preset in PERMISSION_PRESETS:
self.perm_combo.addItem(tr(f"co4e.perm.{preset}"), preset)
idx = self.perm_combo.findData(agent.permission_preset)
self.perm_combo.setCurrentIndex(idx if idx >= 0 else 0)
form.addRow(tr("co4e.f_permission"), self.perm_combo)
self.skills_list = QListWidget()
self.skills_list.setMaximumHeight(120)
for name in skill_names:
it = QListWidgetItem(name)
it.setFlags(it.flags() | Qt.ItemIsUserCheckable)
it.setCheckState(Qt.Checked if name in agent.skills else Qt.Unchecked)
self.skills_list.addItem(it)
form.addRow(tr("co4e.f_skills"), self.skills_list)
# Attachments — files whose extracted text is fed to the agent at run time.
self.attach_list = QListWidget()
self.attach_list.setMaximumHeight(70)
self._attachments: List[str] = list(getattr(agent, "attachments", []) or [])
self._refresh_attach_list()
attach_add = QPushButton(tr("co4e.attach_add"))
attach_add.setIcon(icon("plus"))
attach_add.clicked.connect(self._add_attachment)
attach_del = QPushButton(tr("co4e.attach_remove"))
attach_del.setIcon(icon("trash"))
attach_del.clicked.connect(self._del_attachment)
ab = QHBoxLayout()
ab.addWidget(attach_add)
ab.addWidget(attach_del)
ab.addStretch(1)
abtn = QWidget(); abtn.setLayout(ab)
form.addRow(tr("co4e.f_attachments"), self.attach_list)
form.addRow("", abtn)
buttons = dialog_buttons(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
form.addRow(buttons)
def _ai_draft(self) -> None:
"""Draft the instructions from the agent's name + role — first asking
for an optional description so the generated instructions can be more
specific/detailed than name+role alone would produce."""
if self.ctx is None:
return
name = self.name_edit.text().strip()
role = self.role_edit.text().strip()
if not name and not role:
return
hint, ok = ask_multiline(
self, tr("co4e.ai_draft_hint_title"), tr("co4e.ai_draft_hint_label"))
if not ok:
return
hint = hint.strip()
self.gen_btn.setEnabled(False)
ctx = self.ctx
def job(worker: AgentWorker):
"""Chạy nền: nhờ model soạn thử phần chỉ dẫn cho agent."""
from ..core.ai_task_planner import generate_agent_prompt
return {"text": generate_agent_prompt(ctx.build_active_provider(), name, role, hint,
cancel=worker.is_cancelled)}
def done(result: dict):
"""Đổ chỉ dẫn vừa soạn vào ô soạn thảo."""
self.gen_btn.setEnabled(True)
if result.get("text"):
self.instructions_edit.setPlainText(result["text"])
w = AgentWorker(job)
w.finished_ok.connect(done)
w.failed.connect(lambda _e: self.gen_btn.setEnabled(True))
self._gen_worker = w
w.start()
def _refresh_attach_list(self) -> None:
"""Vẽ lại danh sách tệp đính kèm (chỉ hiện tên tệp, đường dẫn để trong tooltip)."""
from pathlib import Path as _P
self.attach_list.clear()
for p in self._attachments:
item = QListWidgetItem(_P(p).name)
item.setToolTip(p)
self.attach_list.addItem(item)
def _add_attachment(self) -> None:
"""Thêm tệp đính kèm cho agent."""
from PySide6.QtWidgets import QFileDialog
files, _ = QFileDialog.getOpenFileNames(self, tr("co4e.attach_add"))
for f in files:
if f and f not in self._attachments:
self._attachments.append(f)
self._refresh_attach_list()
def _del_attachment(self) -> None:
"""Gỡ tệp đính kèm đang chọn."""
row = self.attach_list.currentRow()
if 0 <= row < len(self._attachments):
self._attachments.pop(row)
self._refresh_attach_list()
def result_agent(self) -> CustomAgent:
"""Bản ghi agent dựng từ nội dung đang có trên form.
Vai trò luôn viết HOA và không bao giờ rỗng — prompt của bước dùng thẳng
chuỗi này nên để trống sẽ sinh ra câu cụt.
"""
a = self._agent
a.name = self.name_edit.text().strip() or "Agent"
a.role = (self.role_edit.text().strip() or "AGENT").upper()
a.icon = self.icon_edit.currentText().strip()
a.instructions = self.instructions_edit.toPlainText().strip()
a.context = self.context_edit.toPlainText().strip()
a.model = self.model_combo.currentText().strip()
a.permission_preset = self.perm_combo.currentData() or "inherit"
a.skills = [self.skills_list.item(i).text()
for i in range(self.skills_list.count())
if self.skills_list.item(i).checkState() == Qt.Checked]
a.attachments = list(self._attachments)
return a
def _load_models(self) -> None:
"""Nạp danh sách model đang sống vào ô chọn, ở luồng nền."""
if self.ctx is None:
return
from ..core import preview_ai
from ..core.worker import AgentWorker
self.load_btn.setEnabled(False)
ctx = self.ctx
def job(_w):
"""Chạy nền: hỏi mọi provider danh sách model đang dùng được."""
return preview_ai.fetch_live_models(ctx)
def done(result: dict):
"""Gộp model của mọi provider vào ô chọn, giữ nguyên thứ đang chọn."""
self.load_btn.setEnabled(True)
models = []
for lst in (result or {}).values():
models.extend(lst)
cur = self.model_combo.currentText()
self.model_combo.blockSignals(True)
self.model_combo.clear()
self.model_combo.addItems(sorted(set(models)))
self.model_combo.setEditText(cur)
self.model_combo.blockSignals(False)
w = AgentWorker(job)
w.finished_ok.connect(done)
w.failed.connect(lambda _e: self.load_btn.setEnabled(True))
self._worker = w
w.start()