refactor(co4e): co4e_tab.py 1885 -> 389, Co4ETab tách thành 7 mixin
File to nhất còn lại của Gamma. Lâm bàn giao ở 1.885 dòng với 100 method
trong một lớp; chia theo bảy mối quan tâm:
co4e_runs.py 364 chạy flow, 3 chế độ, bảng lịch sử lượt chạy
co4e_chat.py 343 khung chat + đếm token + định tuyến riêng
co4e_layout.py 308 ba khung, bảng cấu hình, bố cục màn hẹp
co4e_sidebar.py 251 thư viện workflow/agent/skill, 4 mục gập
co4e_flow_tabs.py 180 dải tab các flow đang mở
co4e_workflow_crud.py 154 tạo/sửa/xoá/nhân bản workflow
co4e_agents.py 51 agent và skill dùng trong flow
ui/co4e_tab.py 389 __init__, set_project, thư mục output
Mọi file dưới 400 dòng.
MỘT LỖI SUÝT LÀM HỎNG FILE: bản đầu tôi cắt method theo m.lineno, mà lineno
trỏ vào dòng `def`, không tính dòng `@...` phía trên. Decorator bị bỏ lại
thành mồ côi ngay trên một hằng số lớp -> file hỏng cú pháp. Bắt được vì
script tự parse lại sau mỗi lần cắt; nếu chỉ cắt rồi ghi thì đã đẩy lên một
file không import nổi.
Ba vòng sửa mức import tương đối: co4e_tab.py nằm ở ui/ (1 cấp), file mới ở
presentation/co4e/ (2 cấp). Còn co4e_canvas / co4e_config_panel /
co4e_agent_dialog thì VẪN ở ui/, nên `.co4e_canvas` phải thành
`...ui.co4e_canvas` chứ không phải `.co4e_canvas` cùng thư mục.
714 test xanh — trong đó có ~4.000 dòng test đặc tả Lâm viết cho đúng vùng
này, nên việc tách được soi khá kỹ. check_co4e, check_controls_alive,
check_layout_geometry, check_probes_bite đều qua.
Cập nhật đích đột biến thứ ba của check_probes_bite: dải tab flow nay ở
presentation/co4e/co4e_layout.py.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
70a0c2fdcf
commit
0e00bf3c2f
@@ -0,0 +1,154 @@
|
||||
"""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:
|
||||
def _apply_workflow(self, wf: co4e.Workflow) -> None:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
self._wf.name = text.strip() or tr("co4e.untitled")
|
||||
self._sync_active_flow_tab_text()
|
||||
def _add_blank_step(self) -> None:
|
||||
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:
|
||||
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:
|
||||
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
|
||||
Reference in New Issue
Block a user