Hồi quy đã vá
-------------
F-12 Kéo–thả hoặc dán tệp vào ô chat ném NameError. R08 tách `_Input` sang
`chat_input_box.py` nhưng để `_paths_from_mime()` ở lại
`composer_widget.py`, nên hai hàm sự kiện Qt gọi một cái tên không tồn
tại. Bốn hàm dùng chung chuyển sang `composer_mime.py` — module thứ ba
là chỗ duy nhất không lặp lại được lỗi này. Đo lại: cả thả lẫn dán đều
gắn 1 tệp, khớp bản trước refactor.
F-01 Đổi provider thì bộ chọn model AI-Edit không làm gì. Hook cũ kiểm
`folder.ai_model_combo`, thuộc tính R08-T12 đã dời sang
`ai_panel.resolver`. Làm mới vô điều kiện, đúng như tab cũ: lần lấy đầu
tiên hỏng thì đổi provider chính là lúc phải thử lại.
F-07 Hàng chọn kỳ của Dashboard bị đẩy xuống dưới các thẻ số liệu. Hàng này
lọc CẢ BA thẻ con chứ không riêng biểu đồ, nên để nó nằm dưới là bắt
người dùng đọc con số trước khi thấy con số đó tính cho kỳ nào. Kèm
theo: `TokenUsageCardWidget` bị bỏ sót `setContentsMargins(0,0,0,0)`
mà hai thẻ con còn lại đã có, đẩy cả hàng thẻ lệch 9px.
`check_layout_geometry` nay khớp TỪNG BYTE với bản trước refactor.
F-11 Hai lớp khai trùng tên phương thức; Python giữ bản sau nên bản đầu là
mã chết. `co4e_tab.py::showEvent` bản đầu gọi `_narrow_guard.attach()`
và không bao giờ chạy.
Tách file (F-09)
----------------
Bốn file chạm trần 400 dòng, mỗi lần cắt ra một trách nhiệm thật:
graph_renderer.py -> graph_scene_builder.py + graph_export.py
co4e_workflow_service.py -> co4e_run_history.py
json_config_repository.py -> config_sections.py
agents_admin_tab.py -> shared/agent_kind_visuals.py
File cuối còn xoá 3 bản sao của hàm đã có trong `shared/formatters.py`,
giống hệt đến từng dòng — nay định dạng thời gian và avatar không lệch nhau
giữa các bảng Giám sát nữa.
Docstring
---------
41,6% -> 100% (3.478/3.478 định nghĩa production), kể cả module dormant và
phương thức dunder. Toàn bộ phần bổ sung viết bằng tiếng Việt; comment tiếng
Anh có sẵn giữ nguyên — dịch ngược là một đợt riêng.
Seam chưa nối dây (F-05)
------------------------
9 seam mang nhãn `SEAM · dựng <ngày>` kèm hai câu: được nối khi nào, và để
dormant thì hỏng gì. Ngày lấy từ lịch sử git, không phải hạn tự đặt. Gate O
đọc nhãn đó và nhắc khi quá 30 ngày.
859 test xanh · 4/4 cổng CASAN · 19/24 checker khớp từng byte bản cũ.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
187 lines
9.0 KiB
Python
187 lines
9.0 KiB
Python
"""Tạo, sửa, đổi tên, xoá, nhân bản workflow — R08-T09.
|
|
|
|
Chỉ thao tác trên danh sách. Phần chạy một workflow nằm ở ``co4e_runs.py``,
|
|
phần vẽ node nằm ở canvas.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import List, Optional
|
|
from PySide6.QtCore import QSize, Qt
|
|
from PySide6.QtWidgets import QInputDialog, QMenu
|
|
from ...core import co4e
|
|
from ...i18n import tr
|
|
from ...ui.icons import icon
|
|
from ...presentation.co4e.co4e_chat_view import _skill_names
|
|
|
|
|
|
class Co4EWorkflowCrudMixin:
|
|
"""Phần tạo/mở/lưu/xoá luồng của Co4E Studio.
|
|
|
|
Tách khỏi ``Co4ETab`` vì đây là nhóm việc trên *danh sách luồng đã lưu*,
|
|
không phải trên khung vẽ đang mở — dù hai bên nói chuyện với nhau liên tục.
|
|
"""
|
|
def _apply_workflow(self, wf: co4e.Workflow) -> None:
|
|
"""Đưa một luồng lên khung vẽ và chuyển hội thoại sang nhật ký của chính nó.
|
|
|
|
KHÔNG xoá ``_flow_outputs``: mỗi luồng giữ ngữ cảnh tích luỹ riêng, đổi
|
|
tab mà xoá là mất kết quả của luồng khác. Luồng cũ xếp dọc thì tự bẻ lại
|
|
thành trái→phải cho khớp bố cục hiện tại.
|
|
"""
|
|
self._wf = wf
|
|
# Per-flow outputs are kept in self._flow_outputs[wf.id] — do NOT clear
|
|
# here (switching tabs must not wipe another flow's accumulated context).
|
|
# Switch the visible conversation to THIS flow's own log.
|
|
self.chat_stack.setCurrentWidget(self._ensure_flow_log(wf.id))
|
|
self.name_edit.setText(wf.name)
|
|
self.canvas.load(wf.nodes, wf.edges)
|
|
self.config.clear_step()
|
|
if wf.nodes:
|
|
self.canvas.relayout_if_vertical() # convert old top-down flows to left→right
|
|
self.canvas.fit_view()
|
|
self._update_run_btn() # reflect THIS flow's run state
|
|
self._refresh_usage_total() # show THIS flow's token/cost total
|
|
def _new_workflow(self) -> None:
|
|
"""Tạo luồng trống mới, đưa con trỏ vào ô tên và báo trạng thái.
|
|
|
|
Trước đây bấm nút này khi khung vẽ đã có sẵn một luồng trống chưa đặt tên
|
|
sẽ tạo ra một luồng trống y hệt — đúng logic nhưng nhìn không thấy gì đổi,
|
|
nên nút bị hiểu là hỏng. Giờ nói rõ vừa xảy ra chuyện gì và đặt con trỏ
|
|
vào đúng việc tiếp theo: đặt tên.
|
|
"""
|
|
self._open_flow(co4e.new_workflow(tr("co4e.untitled")))
|
|
# Pressing this while the canvas already holds an empty untitled flow
|
|
# produced an identical empty untitled flow — correct, and completely
|
|
# invisible, so the button read as broken. Say what happened and put the
|
|
# cursor where the next thing to do is: naming it.
|
|
self.name_edit.setFocus()
|
|
self.name_edit.selectAll()
|
|
self.status_message.emit(tr("co4e.new_flow_ready"))
|
|
def _selected_wf(self) -> Optional[co4e.Workflow]:
|
|
"""Materialise the selected saved-flow row into a Workflow."""
|
|
item = self.wf_list.currentItem()
|
|
if item is None:
|
|
return None
|
|
_kind, ident = item.data(Qt.UserRole)
|
|
return co4e.get_workflow(ident)
|
|
def _load_selected_workflow(self, *_a) -> None:
|
|
"""Mở luồng đang chọn trong danh sách (hoặc chuyển sang tab của nó nếu đã mở)."""
|
|
wf = self._selected_wf()
|
|
if wf is not None:
|
|
self._open_flow(wf) # open (or focus) its browser-style tab
|
|
def _edit_selected_workflow(self) -> None:
|
|
"""Mở luồng đang chọn để sửa; chưa chọn gì thì nhắc người dùng chọn."""
|
|
wf = self._selected_wf()
|
|
if wf is None:
|
|
self.status_message.emit(tr("co4e.select_flow"))
|
|
return
|
|
self._open_flow(wf)
|
|
def _duplicate_selected_workflow(self) -> None:
|
|
"""Nhân bản luồng đang chọn thành một luồng mới rồi nạp lại danh sách."""
|
|
wf = self._selected_wf()
|
|
if wf is None:
|
|
self.status_message.emit(tr("co4e.select_flow"))
|
|
return
|
|
dup = co4e.duplicate_workflow(wf)
|
|
self._reload_sidebar()
|
|
self.status_message.emit(tr("co4e.duplicated_msg", name=dup.name))
|
|
def _wf_context_menu(self, pos) -> None:
|
|
"""Menu chuột phải trên danh sách luồng: sửa, đổi tên, nhân bản, chạy nền, xoá."""
|
|
lw = self.wf_list
|
|
item = lw.itemAt(pos)
|
|
if item is None:
|
|
return
|
|
lw.setCurrentItem(item)
|
|
_kind, ident = item.data(Qt.UserRole)
|
|
menu = QMenu(lw)
|
|
act_edit = menu.addAction(icon("edit"), tr("co4e.edit"))
|
|
act_rename = menu.addAction(icon("edit"), tr("co4e.rename"))
|
|
act_dup = menu.addAction(icon("branch"), tr("co4e.duplicate"))
|
|
act_run = menu.addAction(icon("play"), tr("co4e.run_bg"))
|
|
act_del = menu.addAction(icon("trash"), tr("co4e.delete"))
|
|
chosen = menu.exec(lw.viewport().mapToGlobal(pos))
|
|
if chosen is act_edit:
|
|
self._edit_selected_workflow()
|
|
elif chosen is act_rename:
|
|
self._rename_workflow(ident)
|
|
elif chosen is act_dup:
|
|
self._duplicate_selected_workflow()
|
|
elif chosen is act_run:
|
|
self._run_selected_in_background()
|
|
elif chosen is act_del:
|
|
self._delete_selected_workflow()
|
|
def _rename_workflow(self, ident: str) -> None:
|
|
"""Rename a saved flow in place (e.g. to match its function/task)."""
|
|
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 = (name or "").strip()
|
|
if not ok or not name:
|
|
return
|
|
wf.name = name
|
|
co4e.save_workflow(wf)
|
|
if self._wf.id == ident:
|
|
self.name_edit.setText(name)
|
|
self._wf.name = name
|
|
self._reload_sidebar()
|
|
self.status_message.emit(tr("co4e.renamed_msg", name=name))
|
|
def _delete_selected_workflow(self) -> None:
|
|
"""Xoá luồng đang chọn khỏi đĩa rồi nạp lại danh sách."""
|
|
item = self.wf_list.currentItem()
|
|
if item is None:
|
|
return
|
|
_kind, ident = item.data(Qt.UserRole)
|
|
co4e.delete_workflow(ident)
|
|
self._reload_sidebar()
|
|
def _sync_wf_from_canvas(self) -> None:
|
|
"""Chép node, cạnh và tên từ khung vẽ về đối tượng luồng trước khi lưu."""
|
|
self._wf.nodes = self.canvas.nodes()
|
|
self._wf.edges = self.canvas.edges()
|
|
self._wf.name = self.name_edit.text().strip() or tr("co4e.untitled")
|
|
def _save(self, as_template: bool) -> None:
|
|
"""Lưu luồng đang mở; ``as_template`` đánh dấu nó là mẫu dùng lại được."""
|
|
self._sync_wf_from_canvas()
|
|
self._wf.is_template = as_template
|
|
co4e.save_workflow(self._wf)
|
|
self._reload_sidebar()
|
|
self.status_message.emit(tr("co4e.saved_msg", name=self._wf.name))
|
|
def _autosave(self) -> None:
|
|
"""Tự lưu sau mỗi thay đổi trên khung vẽ — nhưng chỉ với luồng ĐÃ tồn tại
|
|
trên đĩa, để luồng nháp chưa đặt tên không tự nhảy vào danh sách.
|
|
"""
|
|
if co4e.get_workflow(self._wf.id) is not None:
|
|
self._sync_wf_from_canvas()
|
|
co4e.save_workflow(self._wf)
|
|
def _on_name_changed(self, text: str) -> None:
|
|
"""Gõ tên mới thì cập nhật luôn nhãn tab của luồng đang mở."""
|
|
self._wf.name = text.strip() or tr("co4e.untitled")
|
|
self._sync_active_flow_tab_text()
|
|
def _add_blank_step(self) -> None:
|
|
"""Thêm một bước trống vào giữa khung nhìn hiện tại."""
|
|
self.canvas.add_palette_step(co4e.Step(label="New Step"),
|
|
self.canvas.mapToScene(self.canvas.rect().center()))
|
|
def _on_node_selected(self, node_id: str) -> None:
|
|
"""Chọn một node thì nạp bước đó vào bảng thuộc tính, tự mở bảng nếu đang gập."""
|
|
for n in self.canvas.nodes():
|
|
if n.id == node_id:
|
|
self.config.load_step(node_id, n.data, _skill_names())
|
|
if self._config_collapsed:
|
|
self._toggle_config()
|
|
return
|
|
def _on_config_changed(self) -> None:
|
|
"""Sửa thuộc tính bước thì vẽ lại mọi node (nhãn/model có thể đổi) rồi tự lưu."""
|
|
for n in self.canvas.nodes():
|
|
self.canvas.refresh_node(n.id)
|
|
self._autosave()
|
|
def _wf_by_id(self, wf_id: str) -> Optional[co4e.Workflow]:
|
|
"""Resolve a flow id to a Workflow — saved, or the open canvas."""
|
|
wf = co4e.get_workflow(wf_id)
|
|
if wf is not None:
|
|
return wf
|
|
if self._wf.id == wf_id:
|
|
self._sync_wf_from_canvas()
|
|
return self._wf
|
|
return None
|