2026-09-09 16:46:15 +00:00
committed by gitea-admin
co-authored by duylh19
parent 13e2c22067
commit 1b8429e33a
147 changed files with 20993 additions and 461 deletions
+7 -5
View File
@@ -35,7 +35,7 @@ from __future__ import annotations
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QHBoxLayout, QPushButton, QVBoxLayout, QWidget
from ...i18n import tr
from ...i18n import bind_text, bind_tip
from ...ui.icons import icon
from .palette_list import _PaletteList
@@ -55,9 +55,11 @@ class AgentListPanel(QWidget):
def __init__(self, parent: QWidget | None = None) -> None:
"""Danh sách agent ở cột trái Co4E Studio, kèm nút tạo mới."""
super().__init__(parent)
self.new_btn = QPushButton(tr("co4e.new"))
# Bound, not set once: this panel has no retranslate hook of its own, and
# Co4ETab (which owns the language callback) cannot reach these tooltips.
self.new_btn = bind_text(QPushButton(), "co4e.new")
self.new_btn.setIcon(icon("plus"))
self.new_btn.setToolTip(tr("co4e.tt_new_agent"))
bind_tip(self.new_btn, "co4e.tt_new_agent")
self.new_btn.setObjectName("co4eSectionAction")
self.new_btn.setFlat(True)
self.new_btn.setCursor(Qt.PointingHandCursor)
@@ -75,11 +77,11 @@ class AgentListPanel(QWidget):
# Edit/delete act on the selected row, so they stay with the list.
self.edit_btn = QPushButton()
self.edit_btn.setIcon(icon("edit"))
self.edit_btn.setToolTip(tr("co4e.tt_edit_agent"))
bind_tip(self.edit_btn, "co4e.tt_edit_agent")
self.edit_btn.setFixedWidth(34)
self.del_btn = QPushButton()
self.del_btn.setIcon(icon("trash"))
self.del_btn.setToolTip(tr("co4e.tt_del_agent"))
bind_tip(self.del_btn, "co4e.tt_del_agent")
self.del_btn.setFixedWidth(34)
# KHONG noi .clicked o day: cung ly do nhu new_btn o tren.
btns.addWidget(self.edit_btn)
+5 -4
View File
@@ -34,6 +34,7 @@ from PySide6.QtGui import QBrush, QColor, QPainterPath, QPen, QPolygonF
from PySide6.QtWidgets import QGraphicsItem, QGraphicsObject, QGraphicsPathItem, QMenu
from ...core.co4e import STEP_DONE, STEP_ERROR, STEP_PLANNED, STEP_RUNNING, Edge, Node
from ...i18n import tr
from ...theme import current_palette
from .canvas_geometry import _elide, _rounded_path
@@ -233,9 +234,9 @@ class _NodeItem(QGraphicsObject):
def contextMenuEvent(self, e):
"""Menu chuột phải trên node: thêm bước kế, nối từ đây, xoá bước."""
menu = QMenu()
a_add = menu.addAction("+ Add next step")
a_conn = menu.addAction("→ Connect from here")
a_del = menu.addAction("🗑 Delete step")
a_add = menu.addAction("+ " + tr("co4e.canvas_add_next"))
a_conn = menu.addAction("→ " + tr("co4e.canvas_connect_from"))
a_del = menu.addAction("🗑 " + tr("co4e.delete_step"))
chosen = menu.exec(e.screenPos())
if chosen is a_add:
self.canvas.add_step_below(self.node.id)
@@ -342,7 +343,7 @@ class _EdgeItem(QGraphicsPathItem):
def contextMenuEvent(self, e):
"""Menu chuột phải trên đường nối: xoá liên kết."""
menu = QMenu()
act_del = menu.addAction("🗑 Delete connection")
act_del = menu.addAction("🗑 " + tr("co4e.canvas_delete_edge"))
if menu.exec(e.screenPos()) is act_del:
self.canvas.delete_edge(self.edge)
e.accept()
+7 -1
View File
@@ -13,7 +13,7 @@ from PySide6.QtWidgets import QSplitter, QWidget
from ...core import co4e, skills as skills_mod
from ...core.co4e_builtins import BUILTIN_AGENTS
from ...core.worker import AgentWorker
from ...i18n import tr
from ...i18n import bind_dynamic, tr
from ...ui.chat_view import ChatView
from ...ui.icons import icon
from ...presentation.co4e.co4e_chat_view import ChatPanel
@@ -55,6 +55,12 @@ class Co4EChatMixin:
self._co4e_routed_provider = None # routing provider override for the next turn
self._vsplit_sizes = [540, 220] # sizes to restore when expanded
self._msgs_collapsed = True
# The tooltip names the action the button would perform, so it depends on
# which way the box is folded — and the fold state lives here, not in the
# panel. Bound so a language change re-reads it instead of freezing the
# wording set when the tab was built.
bind_dynamic(self.chat_toggle_btn, lambda: self.chat_toggle_btn.setToolTip(
tr("co4e.tt_expand_msgs" if self._msgs_collapsed else "co4e.tt_collapse_msgs")))
return panel
def _toggle_messages(self) -> None:
"""Show/hide the WHOLE chat box (message list + composer) below the
+6 -4
View File
@@ -48,7 +48,7 @@ from PySide6.QtWidgets import (
from ...core import co4e, skills as skills_mod
from ...core.co4e_builtins import BUILTIN_AGENTS
from ...i18n import tr
from ...i18n import bind_placeholder, bind_text, tr
from ...theme import current_palette
from ...ui.icons import icon
from ...ui.routing_toggle import RoutingToggle
@@ -223,7 +223,8 @@ class ChatPanel(QWidget):
self.header = QWidget(); self.header.setObjectName("msgHeader")
mh = QHBoxLayout(self.header); mh.setContentsMargins(6, 3, 6, 3); mh.setSpacing(6)
self.msgs_icon = QLabel(); self.msgs_icon.setPixmap(icon("message").pixmap(14, 14))
self.msgs_title = QLabel(tr("co4e.messages")); self.msgs_title.setObjectName("hint")
self.msgs_title = bind_text(QLabel(), "co4e.messages")
self.msgs_title.setObjectName("hint")
self.chat_toggle_btn = QPushButton()
self.chat_toggle_btn.setObjectName("msgToggle")
self.chat_toggle_btn.setFlat(True)
@@ -253,10 +254,11 @@ class ChatPanel(QWidget):
crow.addWidget(self.usage_total_lbl)
_inp = QWidget(); row = QHBoxLayout(_inp); row.setContentsMargins(0, 0, 0, 0)
self.chat_input = _ChatInput()
self.chat_input.setPlaceholderText(tr("co4e.chat_placeholder"))
bind_placeholder(self.chat_input, "co4e.chat_placeholder")
# KHONG noi .submit o day: cung ly do nhu chat_toggle_btn o tren
# (ben goi noi toi _chat_send cua chinh no).
self.chat_send_btn = QPushButton(tr("co4e.send")); self.chat_send_btn.setIcon(icon("send"))
self.chat_send_btn = bind_text(QPushButton(), "co4e.send")
self.chat_send_btn.setIcon(icon("send"))
# KHONG noi .clicked o day: cung ly do nhu tren.
row.addWidget(self.chat_input, 1)
# Off/Auto/Manual routing toggle for Co4E (surface key "co4e").
+4 -2
View File
@@ -14,7 +14,7 @@ from typing import List
from PySide6.QtCore import QSize, Qt
from PySide6.QtWidgets import QComboBox, QFrame, QHBoxLayout, QLabel, QLineEdit, QPushButton, QScrollArea, QSizePolicy, QSpacerItem, QSplitter, QTabBar, QTabWidget, QVBoxLayout, QWidget
from ...core import co4e
from ...i18n import tr
from ...i18n import bind_text, tr
from ...theme import current_palette
from ...ui.co4e_canvas import Co4ECanvas
from ...ui.icons import icon
@@ -141,7 +141,9 @@ class Co4ELayoutMixin:
self.runs_btn.setToolTip(tr("co4e.tt_runs_tab"))
self.runs_btn.toggled.connect(self._show_runs)
bar.addWidget(QLabel(tr("co4e.flow_name")))
# Bound: nothing else holds this label, so a one-shot tr() here would
# leave "Flow" stuck in the language the toolbar was built in.
bar.addWidget(bind_text(QLabel(), "co4e.flow_name"))
bar.addWidget(self.name_edit, 1)
bar.addWidget(self.add_step_btn)
bar.addWidget(self.save_btn)
+15 -13
View File
@@ -39,7 +39,7 @@ from PySide6.QtWidgets import (
QWidget,
)
from ...i18n import tr
from ...i18n import bind_text, bind_tip
from ...ui.icons import icon
@@ -66,13 +66,15 @@ class RunsPagePanel(QWidget):
hdr = QHBoxLayout()
# The Runs page covers the flow toolbar, so it carries its own way back —
# otherwise the toggle that opened it is off screen.
self.back_btn = QPushButton(tr("co4e.back_to_flow"))
# Bound, not set once: this panel has no retranslate hook of its own, and
# Co4ETab (which owns the language callback) cannot reach these strings.
self.back_btn = bind_text(QPushButton(), "co4e.back_to_flow")
self.back_btn.setIcon(icon("chevron-left"))
self.back_btn.setToolTip(tr("co4e.tt_back_to_flow"))
bind_tip(self.back_btn, "co4e.tt_back_to_flow")
# KHONG noi .clicked o day: ben goi (Co4ETab) tu quyet dinh slot nao
# xu ly - panel chi dung widget, khong biet _show_runs la gi.
hdr.addWidget(self.back_btn)
self.title_label = QLabel(tr("co4e.running_flows"))
self.title_label = bind_text(QLabel(), "co4e.running_flows")
self.title_label.setObjectName("hint")
hdr.addWidget(self.title_label)
# Show + open the workspace folder where flow outputs land (below the tab,
@@ -85,21 +87,21 @@ class RunsPagePanel(QWidget):
# ca hai deu thuoc Co4ETab (can ctx/manager de biet duong dan that).
hdr.addWidget(self.ws_folder_btn)
hdr.addStretch(1)
self.stop_btn = QPushButton(tr("co4e.stop"))
self.stop_btn = bind_text(QPushButton(), "co4e.stop")
self.stop_btn.setIcon(icon("stop"))
self.stop_btn.setObjectName("danger")
self.stop_btn.setToolTip(tr("co4e.tt_stop_run"))
bind_tip(self.stop_btn, "co4e.tt_stop_run")
# KHONG noi .clicked o day: cung ly do nhu back_btn o tren.
self.rename_btn = QPushButton(tr("co4e.rename_run"))
self.rename_btn = bind_text(QPushButton(), "co4e.rename_run")
self.rename_btn.setIcon(icon("edit"))
self.rename_btn.setToolTip(tr("co4e.tt_rename_run"))
bind_tip(self.rename_btn, "co4e.tt_rename_run")
# KHONG noi .clicked o day: cung ly do nhu back_btn o tren.
self.del_btn = QPushButton(tr("co4e.delete_run"))
self.del_btn = bind_text(QPushButton(), "co4e.delete_run")
self.del_btn.setIcon(icon("trash"))
self.del_btn.setToolTip(tr("co4e.tt_delete_run"))
bind_tip(self.del_btn, "co4e.tt_delete_run")
# KHONG noi .clicked o day: cung ly do nhu back_btn o tren.
self.clear_btn = QPushButton(tr("co4e.clear_done"))
self.clear_btn.setToolTip(tr("co4e.tt_clear_runs"))
self.clear_btn = bind_text(QPushButton(), "co4e.clear_done")
bind_tip(self.clear_btn, "co4e.tt_clear_runs")
# KHONG noi .clicked o day: cung ly do nhu back_btn o tren. (Ban goc
# noi thang toi lambda: self.manager.clear_finished(), khong qua mot
# method rieng - Co4ETab van giu dung quirk do khi noi lai signal nay.)
@@ -113,7 +115,7 @@ class RunsPagePanel(QWidget):
self.table.verticalHeader().setVisible(False)
self.table.setEditTriggers(QTableWidget.NoEditTriggers)
self.table.setSelectionBehavior(QTableWidget.SelectRows)
self.table.setToolTip(tr("co4e.tt_runs_list"))
bind_tip(self.table, "co4e.tt_runs_list")
# KHONG noi .itemDoubleClicked o day: cung ly do nhu back_btn o tren.
# Right-click a run → Open / Delete (delete a single old run from history).
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
+4 -4
View File
@@ -13,7 +13,7 @@ import re
from pathlib import Path
from typing import Dict, List, Optional
from PySide6.QtCore import QSize, Qt
from PySide6.QtWidgets import QInputDialog, QMenu, QMessageBox, QTableWidget, QTableWidgetItem
from PySide6.QtWidgets import QMenu, QMessageBox, QTableWidget, QTableWidgetItem
from ...core import co4e
from ...i18n import tr
from ...theme import current_palette
@@ -337,9 +337,9 @@ class Co4ERunsMixin:
h = self.manager.get(run_id)
if h is None:
return
from PySide6.QtWidgets import QInputDialog
new, ok = QInputDialog.getText(self, tr("co4e.rename_run"),
tr("co4e.rename_run_label"), text=h.name)
from ...ui.dialog_buttons import ask_text
new, ok = ask_text(self, tr("co4e.rename_run"),
tr("co4e.rename_run_label"), text=h.name)
new = (new or "").strip()
if not ok or not new or new == h.name:
return
+53 -15
View File
@@ -11,14 +11,42 @@ from typing import List
from PySide6.QtCore import QSize, Qt
from PySide6.QtWidgets import QHBoxLayout, QListWidget, QListWidgetItem, QPushButton, QSplitter, QVBoxLayout, QWidget
from ...core import co4e, skills as skills_mod
from ...i18n import tr
from ...i18n import bind_dynamic, bind_text, bind_tip, tr
from ...ui.icons import icon
from ...presentation.co4e.agent_list_panel import AgentListPanel
from ...presentation.co4e.co4e_chat_view import _skill_names
from ...presentation.co4e.palette_list import _PaletteList
from ...presentation.co4e.skills_list_panel import SkillsListPanel
def _skill_prefix_lookup(all_skills):
"""Answer ``skills.skill_prefix_for`` from an ALREADY-LOADED skill list.
``skill_prefix_for`` re-reads the whole skill folder on every call, so
asking it once per skill made a sidebar reload cost one full disk scan per
skill — measured at ~3.8s of frozen GUI thread on a 121-skill library, and
that reload runs on every language switch.
The scan order and the blank-instructions rule are copied from
``skill_prefix_for`` deliberately: a namesake with no instructions must NOT
end the search, or a skill's text silently becomes empty in an agent prompt.
"""
cache: dict = {}
def lookup(name: str) -> str:
"""The ``## Skill: <name>\\n<instructions>`` block for one name, or ''."""
if not name:
return ""
low = name.strip().lower()
if low not in cache:
cache[low] = next(
(f"## Skill: {s.name}\n{s.instructions.strip()}" for s in all_skills
if (s.slug == low or s.name.lower() == low) and s.instructions.strip()),
"")
return cache[low]
return lookup
class Co4ESidebarMixin:
"""Cột trái của Co4E Studio: Workflows, Agents, Skills và Flow Status."""
def _build_sidebar(self) -> QWidget:
@@ -66,9 +94,12 @@ class Co4ESidebarMixin:
col = _Col(self.side_split)
# --- WORKFLOWS ---------------------------------------------------
self.wf_new_btn = QPushButton(tr("co4e.new"))
# Bound, not set once: Co4ETab._retranslate reloads the sidebar's LIST
# CONTENTS, but these headings, buttons and tooltips are built here and
# nothing re-applied them — they stayed in the language of app start-up.
self.wf_new_btn = bind_text(QPushButton(), "co4e.new")
self.wf_new_btn.setIcon(icon("plus"))
self.wf_new_btn.setToolTip(tr("co4e.tt_new_wf"))
bind_tip(self.wf_new_btn, "co4e.tt_new_wf")
self.wf_new_btn.setObjectName("co4eSectionAction")
self.wf_new_btn.setFlat(True)
self.wf_new_btn.setCursor(Qt.PointingHandCursor)
@@ -78,7 +109,7 @@ class Co4ESidebarMixin:
# Draggable: drag a flow onto the canvas to merge it in (Nova-style);
# double-click loads it onto the canvas.
self.wf_list = _PaletteList(payload_role=Qt.UserRole + 2)
self.wf_list.setToolTip(tr("co4e.drag_hint"))
bind_tip(self.wf_list, "co4e.drag_hint")
self.wf_list.itemDoubleClicked.connect(self._load_selected_workflow)
self.wf_list.setContextMenuPolicy(Qt.CustomContextMenu)
self.wf_list.customContextMenuRequested.connect(self._wf_context_menu)
@@ -93,8 +124,9 @@ class Co4ESidebarMixin:
wl.addLayout(wf_btns)
# Its own row: sharing one line with the three icon buttons cut "Chạy
# nền" down to "Chạ" as soon as the sidebar hit its narrow width.
self.wf_runbg_btn = QPushButton(tr("co4e.run_bg")); self.wf_runbg_btn.setIcon(icon("play"))
self.wf_runbg_btn.setToolTip(tr("co4e.tt_run_bg"))
self.wf_runbg_btn = bind_text(QPushButton(), "co4e.run_bg")
self.wf_runbg_btn.setIcon(icon("play"))
bind_tip(self.wf_runbg_btn, "co4e.tt_run_bg")
self.wf_runbg_btn.clicked.connect(self._run_selected_in_background)
wl.addWidget(self.wf_runbg_btn)
col.addWidget(self._section("co4e.tab_workflows", wf_body, self.wf_new_btn), 3)
@@ -135,12 +167,12 @@ class Co4ESidebarMixin:
self.runs_more_btn.setIcon(icon("chevron-right"))
self.runs_more_btn.setFixedWidth(30)
self.runs_more_btn.setFlat(True)
self.runs_more_btn.setToolTip(tr("co4e.tt_runs_tab"))
bind_tip(self.runs_more_btn, "co4e.tt_runs_tab")
self.runs_more_btn.clicked.connect(lambda: self._show_runs(True))
runs_body = QWidget(); rl = QVBoxLayout(runs_body)
rl.setContentsMargins(0, 0, 0, 0); rl.setSpacing(4)
self.runs_side_list = QListWidget()
self.runs_side_list.setToolTip(tr("co4e.tt_runs_tab"))
bind_tip(self.runs_side_list, "co4e.tt_runs_tab")
self.runs_side_list.itemClicked.connect(self._on_side_run_clicked)
rl.addWidget(self.runs_side_list, 1)
col.addWidget(self._section("co4e.runs_tab", runs_body, self.runs_more_btn), 2)
@@ -202,7 +234,10 @@ class Co4ESidebarMixin:
v.addWidget(body, 1)
self._sections[key] = (head, body, stretch)
self._sync_section_arrow(key)
# bind_dynamic, not bind_text: the heading is the fold arrow plus the
# translated name in caps, so re-applying it means re-running the whole
# line rather than pushing one key into setText.
bind_dynamic(head, lambda k=key: self._sync_section_arrow(k))
return box
def _fold_section(self, key: str, body: QWidget, box: QWidget, on: bool) -> None:
"""Fold/unfold a section AND give its height back to the others.
@@ -223,7 +258,7 @@ class Co4ESidebarMixin:
head.setText(("▾ " if head.isChecked() else "▸ ") + tr(key).upper())
def _icon_btn(self, icon_name: str, tip_key: str, slot) -> QPushButton:
"""Dựng một nút icon nhỏ (rộng 34px) kèm tooltip cho hàng công cụ của mục."""
b = QPushButton(); b.setIcon(icon(icon_name)); b.setToolTip(tr(tip_key))
b = QPushButton(); b.setIcon(icon(icon_name)); bind_tip(b, tip_key)
b.setFixedWidth(34)
b.clicked.connect(slot)
return b
@@ -254,13 +289,16 @@ class Co4ESidebarMixin:
co4e._step_dict(step))
it.setData(Qt.UserRole + 1, ca.id)
self.agent_list.addItem(it)
# Skills
# Skills — the library is read ONCE here and both the names and the
# instructions come out of that one read (see _skill_prefix_lookup).
self.skill_list.clear()
for name in _skill_names():
content = skills_mod.skill_prefix_for(name)
all_skills = skills_mod.list_skills() + skills_mod.builtin_skills()
skill_prefix = _skill_prefix_lookup(all_skills)
for skill in all_skills:
name = skill.name
payload = co4e._step_dict(co4e.Step(
label=name, agent_slug=co4e.slugify(name), role="SKILL", icon="sparkle",
instructions=content, skills=[name]))
instructions=skill_prefix(name), skills=[name]))
self.skill_list.addItem(self._palette_item(name, "sparkle", payload))
@staticmethod
def _palette_item(text: str, icon_name: str, payload: dict) -> QListWidgetItem:
+4 -3
View File
@@ -8,10 +8,11 @@ from __future__ import annotations
import re
from typing import List, Optional
from PySide6.QtCore import QSize, Qt
from PySide6.QtWidgets import QInputDialog, QMenu
from PySide6.QtWidgets import QMenu
from ...core import co4e
from ...core.co4e import STEP_DONE, STEP_RUNNING
from ...i18n import tr
from ...ui.dialog_buttons import ask_text
from ...ui.icons import icon
from ...presentation.co4e.co4e_chat_view import _skill_names
@@ -118,8 +119,8 @@ class Co4EWorkflowCrudMixin:
wf = co4e.get_workflow(ident)
if wf is None:
return
name, ok = QInputDialog.getText(self, tr("co4e.rename"), tr("co4e.rename_prompt"),
text=wf.name)
name, ok = ask_text(self, tr("co4e.rename"), tr("co4e.rename_prompt"),
text=wf.name)
name = (name or "").strip()
if not ok or not name:
return
@@ -24,20 +24,21 @@ thứ tự kế thừa không ảnh hưởng hành vi (khác trường hợp
``co4e_canvas_widget.py``, nơi thứ tự mixin-trước-Qt-base là bắt buộc vì có
override trùng tên).
Import trong từng method giữ nguyên y hệt bản gốc (kể cả các import cục bộ có
vẻ thừa như ``from PySide6.QtWidgets import QInputDialog`` lặp lại bên trong
``_add_subagent``/``_edit_subagent`` dù đã có ở top-level) — chỉ số cấp `..`
được nâng lên `...` cho khớp việc file dời từ ``ui/`` (cách gốc 2 cấp) sang
``presentation/co4e/`` (cách gốc 3 cấp).
Import trong từng method giữ nguyên y hệt bản gốc — chỉ số cấp `..` được nâng
lên `...` cho khớp việc file dời từ ``ui/`` (cách gốc 2 cấp) sang
``presentation/co4e/`` (cách gốc 3 cấp). Riêng các lời gọi ``QInputDialog``
đã chuyển sang ``ui.dialog_buttons``: hàm tĩnh của Qt tự dựng hộp thoại bên
trong nên nút "Cancel" của nó luôn là tiếng Anh.
"""
from __future__ import annotations
from typing import List
from PySide6.QtWidgets import QInputDialog, QListWidgetItem
from PySide6.QtWidgets import QListWidgetItem
from ...core.co4e import SubAgent
from ...i18n import tr
from ...ui.dialog_buttons import ask_item, ask_multiline, ask_text
class _StepConfigActionsMixin:
@@ -67,14 +68,12 @@ class _StepConfigActionsMixin:
"""Thêm một sub-agent vào bước đang chọn (chạy song song trong bước đó)."""
if self._step is None:
return
from PySide6.QtWidgets import QInputDialog
names = self._available_agent_names()
if names:
name, ok = QInputDialog.getItem(self, tr("co4e.pick_agent"), tr("co4e.pick_agent"),
names, 0, True) # editable: can type a new one
name, ok = ask_item(self, tr("co4e.pick_agent"), tr("co4e.pick_agent"),
names, 0, True) # editable: can type a new one
else:
name, ok = QInputDialog.getText(self, tr("co4e.pick_agent"), tr("co4e.pick_agent"))
name, ok = ask_text(self, tr("co4e.pick_agent"), tr("co4e.pick_agent"))
name = (name or "").strip()
if not ok or not name:
return
@@ -89,13 +88,11 @@ class _StepConfigActionsMixin:
row = self.sub_list.row(item)
if not (0 <= row < len(self._step.sub_agents)):
return
from PySide6.QtWidgets import QInputDialog
names = self._available_agent_names()
cur = self._step.sub_agents[row].agent
start = names.index(cur) if cur in names else 0
name, ok = QInputDialog.getItem(self, tr("co4e.pick_agent"), tr("co4e.pick_agent"),
names or [cur], start, True)
name, ok = ask_item(self, tr("co4e.pick_agent"), tr("co4e.pick_agent"),
names or [cur], start, True)
name = (name or "").strip()
if ok and name:
self._step.sub_agents[row].agent = name
@@ -151,7 +148,7 @@ class _StepConfigActionsMixin:
role = self.role_edit.text().strip()
if not name and not role:
return
hint, ok = QInputDialog.getMultiLineText(
hint, ok = ask_multiline(
self, tr("co4e.ai_draft_hint_title"), tr("co4e.ai_draft_hint_label"))
if not ok:
return
+40 -32
View File
@@ -39,7 +39,7 @@ from PySide6.QtWidgets import (
from ...config import PROVIDER_LABELS
from ...core.co4e import PERMISSION_PRESETS, STEP_DONE, STEP_RUNNING, Step
from ...i18n import tr
from ...i18n import bind_items, bind_placeholder, bind_text, bind_tip, tr
from ...ui.icons import icon, icon_picker_combo
from .node_property_actions_mixin import _StepConfigActionsMixin
from .step_config_section import _add_section
@@ -79,30 +79,34 @@ class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
# (skills/files/sub-agents). No tabs/accordion: every group's border
# and heading are what separate it from its neighbours, and all three
# are on screen (or one scroll away) at once.
form, _basic_card = _add_section(outer, tr("co4e.tab_basic"))
form, _basic_card = _add_section(outer, "co4e.tab_basic")
self.label_edit = QLineEdit()
self.label_edit.textChanged.connect(self._on_edit)
form.addRow(tr("co4e.f_label"), self.label_edit)
form.addRow(bind_text(QLabel(), "co4e.f_label"), self.label_edit)
self.role_edit = QLineEdit()
self.role_edit.textChanged.connect(self._on_edit)
form.addRow(tr("co4e.f_role"), self.role_edit)
form.addRow(bind_text(QLabel(), "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()
self.icon_edit.lineEdit().setPlaceholderText(tr("co4e.f_icon_placeholder"))
# Kept on self because the combo's line edit belongs to C++: a binding
# holds its widget weakly, so with no owner on this side the Python
# wrapper could be collected and the binding silently dropped.
self._icon_line = self.icon_edit.lineEdit()
bind_placeholder(self._icon_line, "co4e.f_icon_placeholder")
self.icon_edit.currentTextChanged.connect(self._on_edit)
form.addRow(tr("co4e.f_icon"), self.icon_edit)
form.addRow(bind_text(QLabel(), "co4e.f_icon"), self.icon_edit)
self.instructions_edit = QPlainTextEdit()
self.instructions_edit.setMaximumHeight(120)
self.instructions_edit.textChanged.connect(self._on_edit)
self.gen_btn = QPushButton(tr("co4e.ai_draft"))
self.gen_btn = bind_text(QPushButton(), "co4e.ai_draft")
self.gen_btn.setIcon(icon("sparkle"))
self.gen_btn.setToolTip(tr("co4e.ai_draft_tooltip"))
bind_tip(self.gen_btn, "co4e.ai_draft_tooltip")
self.gen_btn.setEnabled(ctx is not None)
self.gen_btn.clicked.connect(self._ai_draft)
instr_box = QWidget()
@@ -110,17 +114,17 @@ class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
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)
form.addRow(bind_text(QLabel(), "co4e.f_instructions"), instr_box)
# Extra context — free-text background/info fed to the step at run time
# (in addition to instructions, attachments and upstream outputs).
self.context_edit = QPlainTextEdit()
self.context_edit.setMaximumHeight(90)
self.context_edit.setPlaceholderText(tr("co4e.f_context_placeholder"))
bind_placeholder(self.context_edit, "co4e.f_context_placeholder")
self.context_edit.textChanged.connect(self._on_edit)
form.addRow(tr("co4e.f_context"), self.context_edit)
form.addRow(bind_text(QLabel(), "co4e.f_context"), self.context_edit)
form2, _model_card = _add_section(outer, tr("co4e.tab_model_perm"))
form2, _model_card = _add_section(outer, "co4e.tab_model_perm")
model_row = QHBoxLayout()
self.model_combo = QComboBox()
@@ -128,48 +132,52 @@ class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
self.model_combo.editTextChanged.connect(self._on_edit)
self.load_models_btn = QPushButton()
self.load_models_btn.setIcon(icon("download"))
self.load_models_btn.setToolTip(tr("co4e.load_models_tooltip"))
bind_tip(self.load_models_btn, "co4e.load_models_tooltip")
self.load_models_btn.clicked.connect(self._load_models)
self.load_models_btn.setEnabled(ctx is not None)
model_row.addWidget(self.model_combo, 1)
model_row.addWidget(self.load_models_btn)
mrow = QWidget(); mrow.setLayout(model_row)
form2.addRow(tr("co4e.f_model"), mrow)
form2.addRow(bind_text(QLabel(), "co4e.f_model"), mrow)
self.perm_combo = QComboBox()
for preset in PERMISSION_PRESETS:
self.perm_combo.addItem(tr(f"co4e.perm.{preset}"), preset)
perm_keys = [f"co4e.perm.{preset}" for preset in PERMISSION_PRESETS]
for preset, key in zip(PERMISSION_PRESETS, perm_keys):
self.perm_combo.addItem(tr(key), preset)
# Only the visible labels follow the language — the data column stays
# the preset id that ``_on_edit`` persists onto the Step.
bind_items(self.perm_combo, perm_keys)
self.perm_combo.currentIndexChanged.connect(self._on_edit)
form2.addRow(tr("co4e.f_permission"), self.perm_combo)
form2.addRow(bind_text(QLabel(), "co4e.f_permission"), self.perm_combo)
verify_row = QHBoxLayout()
self.verify_chk = QCheckBox(tr("co4e.f_self_verify"))
self.verify_chk = bind_text(QCheckBox(), "co4e.f_self_verify")
self.verify_chk.toggled.connect(self._on_edit)
self.rounds_spin = QSpinBox()
self.rounds_spin.setRange(1, 5)
self.rounds_spin.valueChanged.connect(self._on_edit)
verify_row.addWidget(self.verify_chk)
verify_row.addWidget(QLabel(tr("co4e.f_verify_rounds")))
verify_row.addWidget(bind_text(QLabel(), "co4e.f_verify_rounds"))
verify_row.addWidget(self.rounds_spin)
verify_row.addStretch(1)
vrow = QWidget(); vrow.setLayout(verify_row)
form2.addRow("", vrow)
form3, _skills_card = _add_section(outer, tr("co4e.tab_skills_files"))
form3, _skills_card = _add_section(outer, "co4e.tab_skills_files")
# Skills checklist (registry skills)
self.skills_list = QListWidget()
self.skills_list.setMaximumHeight(110)
self.skills_list.itemChanged.connect(self._on_edit)
form3.addRow(tr("co4e.f_skills"), self.skills_list)
form3.addRow(bind_text(QLabel(), "co4e.f_skills"), self.skills_list)
# Attachments — files whose extracted text is fed to this step at run time.
self.attach_list = QListWidget()
self.attach_list.setMaximumHeight(80)
self.attach_add_btn = QPushButton(tr("co4e.attach_add"))
self.attach_add_btn = bind_text(QPushButton(), "co4e.attach_add")
self.attach_add_btn.setIcon(icon("plus"))
self.attach_add_btn.clicked.connect(self._add_attachment)
self.attach_del_btn = QPushButton(tr("co4e.attach_remove"))
self.attach_del_btn = bind_text(QPushButton(), "co4e.attach_remove")
self.attach_del_btn.setIcon(icon("trash"))
self.attach_del_btn.clicked.connect(self._del_attachment)
att_btns = QHBoxLayout()
@@ -177,7 +185,7 @@ class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
att_btns.addWidget(self.attach_del_btn)
att_btns.addStretch(1)
abtn = QWidget(); abtn.setLayout(att_btns)
form3.addRow(tr("co4e.f_attachments"), self.attach_list)
form3.addRow(bind_text(QLabel(), "co4e.f_attachments"), self.attach_list)
form3.addRow("", abtn)
# Parallel sub-agents get their OWN section — same header style as
@@ -185,14 +193,14 @@ class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
# Skills & Tệp, since it's really a distinct group, just one that
# only applies to parallel-variant steps. load_step() hides the whole
# card for a non-parallel step (see is_par below).
form4, self._parallel_card = _add_section(outer, tr("co4e.f_subagents"))
form4, self._parallel_card = _add_section(outer, "co4e.f_subagents")
self.sub_list = QListWidget()
self.sub_list.setMaximumHeight(90)
self.sub_list.itemDoubleClicked.connect(self._edit_subagent) # re-pick agent
self.sub_add_btn = QPushButton(tr("co4e.add_subagent"))
self.sub_add_btn = bind_text(QPushButton(), "co4e.add_subagent")
self.sub_add_btn.setIcon(icon("plus"))
self.sub_add_btn.clicked.connect(self._add_subagent)
self.sub_del_btn = QPushButton(tr("co4e.del_subagent"))
self.sub_del_btn = bind_text(QPushButton(), "co4e.del_subagent")
self.sub_del_btn.setIcon(icon("trash"))
self.sub_del_btn.clicked.connect(self._del_subagent)
sub_btns = QHBoxLayout()
@@ -205,17 +213,17 @@ class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
# Footer actions — one compact row (Run · Run from here · Delete),
# kept below every section, not inside one of the cards.
self.run_btn = QPushButton(tr("co4e.run"))
self.run_btn = bind_text(QPushButton(), "co4e.run")
self.run_btn.setIcon(icon("play"))
self.run_btn.setToolTip(tr("co4e.run_this_step"))
bind_tip(self.run_btn, "co4e.run_this_step")
self.run_btn.clicked.connect(lambda: self.run_node.emit(self._node_id))
self.run_from_btn = QPushButton(tr("co4e.run_from_here"))
self.run_from_btn.setToolTip(tr("co4e.run_from_here"))
self.run_from_btn = bind_text(QPushButton(), "co4e.run_from_here")
bind_tip(self.run_from_btn, "co4e.run_from_here")
self.run_from_btn.clicked.connect(lambda: self.run_from.emit(self._node_id))
self.del_btn = QPushButton()
self.del_btn.setIcon(icon("trash"))
self.del_btn.setObjectName("danger")
self.del_btn.setToolTip(tr("co4e.delete_step"))
bind_tip(self.del_btn, "co4e.delete_step")
self.del_btn.setFixedWidth(38)
self.del_btn.clicked.connect(lambda: self.delete_node.emit(self._node_id))
foot = QHBoxLayout()
+5 -3
View File
@@ -29,7 +29,7 @@ from __future__ import annotations
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QPushButton, QVBoxLayout, QWidget
from ...i18n import tr
from ...i18n import bind_text, bind_tip
from .palette_list import _PaletteList
@@ -50,8 +50,10 @@ class SkillsListPanel(QWidget):
cái gì.
"""
super().__init__(parent)
self.manage_btn = QPushButton(tr("co4e.manage_skills"))
self.manage_btn.setToolTip(tr("co4e.tt_manage_skills"))
# Bound, like AgentListPanel's: the panel owns how its own button reads,
# so no embedder has to remember it in a retranslate method.
self.manage_btn = bind_text(QPushButton(), "co4e.manage_skills")
bind_tip(self.manage_btn, "co4e.tt_manage_skills")
self.manage_btn.setObjectName("co4eSectionAction")
self.manage_btn.setFlat(True)
self.manage_btn.setCursor(Qt.PointingHandCursor)
+16 -4
View File
@@ -26,6 +26,7 @@ from __future__ import annotations
from PySide6.QtCore import Qt, QEasingCurve, QPropertyAnimation, Signal
from PySide6.QtWidgets import QFormLayout, QLabel, QVBoxLayout, QWidget
from ...i18n import bind_dynamic, tr
from ...theme import current_palette
_SECTION_ANIM_MS = 180
@@ -64,7 +65,7 @@ class _SectionHeader(QLabel):
super().showEvent(event)
def _add_section(outer: QVBoxLayout, title: str) -> tuple[QFormLayout, QWidget]:
def _add_section(outer: QVBoxLayout, title_key: str) -> tuple[QFormLayout, QWidget]:
"""One group of fields, collapsed to just its heading by default and
independently expandable, so a long step config reads as a short list of
group names until you open the one you need. Deliberately bare — no card
@@ -74,7 +75,11 @@ def _add_section(outer: QVBoxLayout, title: str) -> tuple[QFormLayout, QWidget]:
the group's rows to ``form``; ``card`` is the whole section (header +
body) — hide it to remove the group entirely (e.g. for a section that
only applies to some steps), rather than hiding individual rows inside
an always-visible header."""
an always-visible header.
Nhận KHOÁ dịch, không nhận chuỗi đã dịch: nhãn mục do hàm này tự dựng nên
nơi gọi không giữ tham chiếu nào để áp lại: truyền ``tr(...)`` vào đây thì
bốn tiêu đề đứng nguyên ở ngôn ngữ lúc dựng panel."""
p = current_palette()
card = QWidget()
card_lay = QVBoxLayout(card)
@@ -96,7 +101,6 @@ def _add_section(outer: QVBoxLayout, title: str) -> tuple[QFormLayout, QWidget]:
# diacritics.
header.ensurePolished()
header.setFixedHeight(header.fontMetrics().height())
header.setText(f"▶ {title}")
card_lay.addWidget(header)
body = QWidget()
@@ -112,6 +116,14 @@ def _add_section(outer: QVBoxLayout, title: str) -> tuple[QFormLayout, QWidget]:
is_open = False
def _sync_header() -> None:
"""Nhãn mục: dấu gập/mở hiện tại + tiêu đề theo ngôn ngữ đang chọn."""
header.setText(f"{'▼' if is_open else '▶'} {tr(title_key)}")
# Ràng buộc ĐỘNG chứ không bind cứng một chuỗi: nhãn này mang cả trạng thái
# gập/mở, nên bind cứng sẽ trả nó về ▶ mỗi lần người dùng đổi ngôn ngữ.
bind_dynamic(header, _sync_header)
def _on_finished() -> None:
"""Hiệu ứng gập/mở chạy xong: bỏ trần chiều cao khi đang mở, để bước có nhiều
trường không bị cắt cụt.
@@ -130,7 +142,7 @@ def _add_section(outer: QVBoxLayout, title: str) -> tuple[QFormLayout, QWidget]:
"""Lật trạng thái gập/mở của một mục và chạy hiệu ứng tương ứng."""
nonlocal is_open
is_open = not is_open
header.setText(f"{'▼' if is_open else '▶'} {title}")
_sync_header()
anim.stop()
if is_open:
body.setVisible(True)