feat(co4e): tách 6 widget UI khỏi ui/co4e_tab.py sang presentation/co4e/*
Lane N3 (Co4E Studio) — dùng bộ workflow refactor-god-file, mỗi bước có characterization test trước khi tách, hậu kiểm ranh giới tầng sau mỗi bước: - skills_list_panel.py / agent_list_panel.py — 2 khu vực sidebar - co4e_canvas_widget.py + canvas_items.py + canvas_interaction_mixin.py — Co4ECanvas tách 3 file (vượt 400 dòng nếu đứng một mình) - node_property_panel.py + node_property_actions_mixin.py + step_config_section.py — StepConfigPanel, cùng lý do - co4e_run_control_widget.py — RunsPagePanel (trang Flow Status) - co4e_chat_view.py — ChatPanel + _ChatInput + helper autocomplete - palette_list.py — _PaletteList dời khỏi ui/co4e_tab.py, hết import ngược presentation -> ui (agent/skills panel giờ import top-level) ui/co4e_tab.py giảm 2089 -> 1878 dòng, chỉ còn phần wiring + business logic (Co4ERunManager/AgentWorker chưa đổi — nằm ngoài phạm vi này, xem docstring presentation/co4e/co4e_tab.py). ui/co4e_canvas.py và ui/co4e_config_panel.py còn lại là compat shim re-export, không đổi API cho bên gọi. Thêm tests/test_co4e_integration.py — dựng thật Co4ETab qua build_co4e_tab(), lái luồng qua nhiều panel trong cùng instance (thêm node, mở/gập chat, chuyển trang Flow Status rồi quay lại không mất state canvas) — bắt lỗi wiring xuyên-panel mà characterization test từng panel riêng không thấy được. Đã xác minh: pytest 348 passed/1 skipped, tools/check_co4e.py sạch, không file nào >400 dòng, domain/application không import PySide6, và so pixel before/after (git worktree tại HEAD cũ) ra 0/1.125.000 pixel khác biệt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+70
-281
@@ -15,13 +15,11 @@ persona and ``/skill:<name>`` applies a skill — same as Cowork.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from PySide6.QtCore import QMimeData, QSize, Qt, Signal
|
||||
from PySide6.QtGui import QDrag
|
||||
from PySide6.QtCore import QSize, Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox, QFrame, QHBoxLayout, QHeaderView, QInputDialog, QLabel, QLineEdit,
|
||||
QListView, QListWidget, QListWidgetItem, QMenu, QMessageBox, QPushButton,
|
||||
@@ -36,9 +34,16 @@ from ..core.worker import AgentWorker
|
||||
from ..i18n import on_language_changed, tr
|
||||
from ..theme import current_palette
|
||||
from .chat_view import ChatView
|
||||
from .co4e_canvas import CO4E_MIME, Co4ECanvas
|
||||
from .co4e_canvas import Co4ECanvas
|
||||
from .co4e_config_panel import StepConfigPanel
|
||||
from .icons import icon
|
||||
from ..presentation.co4e.agent_list_panel import AgentListPanel
|
||||
from ..presentation.co4e.co4e_chat_view import (
|
||||
ChatPanel, _ChatInput, _agent_names, _directive_token, _skill_names,
|
||||
)
|
||||
from ..presentation.co4e.co4e_run_control_widget import RunsPagePanel
|
||||
from ..presentation.co4e.palette_list import _PaletteList
|
||||
from ..presentation.co4e.skills_list_panel import SkillsListPanel
|
||||
|
||||
|
||||
_PLAN_GLYPH = {"completed": "✓", "done": "✓", "in_progress": "▶", "running": "▶",
|
||||
@@ -57,19 +62,6 @@ def _fmt_plan(steps) -> str:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _skill_names() -> List[str]:
|
||||
try:
|
||||
return [s.name for s in skills_mod.list_skills() + skills_mod.builtin_skills()]
|
||||
except Exception: # noqa: BLE001
|
||||
return []
|
||||
|
||||
|
||||
def _agent_names() -> List[str]:
|
||||
names = [a.name for a in co4e.list_custom_agents()]
|
||||
names += [a.name for a in BUILTIN_AGENTS if a.name not in names]
|
||||
return names
|
||||
|
||||
|
||||
class _EqualTabBar(QTabBar):
|
||||
"""Icon-only sidebar tabs (Workflows / Agents / Skills), all the same width,
|
||||
sized to fill the sidebar with a comfortable minimum (~double the default
|
||||
@@ -93,138 +85,6 @@ class _EqualTabBar(QTabBar):
|
||||
self.updateGeometry() # re-hint tab widths when resized
|
||||
|
||||
|
||||
class _PaletteList(QListWidget):
|
||||
"""A list whose rows can be dragged onto the canvas. Each item carries a
|
||||
JSON-able drag payload (a step dict, or a workflow dict) in ``payload_role``.
|
||||
Workflow rows also keep a (kind, id) tuple in Qt.UserRole for load/delete."""
|
||||
|
||||
def __init__(self, parent=None, payload_role=Qt.UserRole):
|
||||
super().__init__(parent)
|
||||
self._payload_role = payload_role
|
||||
self.setDragEnabled(True)
|
||||
self.setDragDropMode(QListWidget.DragOnly)
|
||||
|
||||
def startDrag(self, _actions): # noqa: N802
|
||||
item = self.currentItem()
|
||||
if item is None:
|
||||
return
|
||||
payload = item.data(self._payload_role)
|
||||
if not payload:
|
||||
return
|
||||
md = QMimeData()
|
||||
md.setData(CO4E_MIME, json.dumps(payload).encode("utf-8"))
|
||||
drag = QDrag(self)
|
||||
drag.setMimeData(md)
|
||||
drag.exec(Qt.CopyAction)
|
||||
|
||||
|
||||
def _directive_token(text: str, pos: int):
|
||||
"""Locate a ``/skill[:x]`` or ``/agent[:x]`` directive the cursor is on,
|
||||
anywhere in the line. Returns ``(start, kind, partial)`` or ``None``."""
|
||||
before = text[:pos]
|
||||
start = re.search(r"\S*$", before).start()
|
||||
token = before[start:]
|
||||
m = re.match(r"^/(skill|agent):?([\w\-.]*)$", token)
|
||||
if m:
|
||||
return start, m.group(1), m.group(2)
|
||||
for kind in ("skill", "agent"):
|
||||
if len(token) >= 2 and ("/" + kind).startswith(token):
|
||||
return start, kind, ""
|
||||
return None
|
||||
|
||||
|
||||
class _ChatInput(QLineEdit):
|
||||
"""Chat box with ``/skill:`` and ``/agent:`` autocomplete (parity with the
|
||||
Cowork composer). The popup never grabs focus, so typing keeps flowing."""
|
||||
|
||||
submit = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._popup = QListWidget()
|
||||
self._popup.setWindowFlags(Qt.Tool | Qt.FramelessWindowHint
|
||||
| Qt.WindowStaysOnTopHint | Qt.NoDropShadowWindowHint)
|
||||
self._popup.setAttribute(Qt.WA_ShowWithoutActivating, True)
|
||||
self._popup.setFocusPolicy(Qt.NoFocus)
|
||||
self._popup.itemClicked.connect(lambda _i: self._accept())
|
||||
self.textEdited.connect(self._maybe_popup)
|
||||
|
||||
def _maybe_popup(self, *_a) -> None:
|
||||
tok = _directive_token(self.text(), self.cursorPosition())
|
||||
if tok is None:
|
||||
self._popup.hide()
|
||||
return
|
||||
_start, kind, partial = tok
|
||||
f = partial.lower()
|
||||
self._popup.clear()
|
||||
if kind == "skill":
|
||||
for name in _skill_names():
|
||||
if f in name.lower():
|
||||
self._add_row(name, f"/skill:{co4e.slugify(name)} ", name)
|
||||
else:
|
||||
for name in _agent_names():
|
||||
if f in name.lower():
|
||||
self._add_row(name, f"/agent:{name} ", name)
|
||||
if self._popup.count() == 0:
|
||||
self._popup.hide()
|
||||
return
|
||||
self._popup.setCurrentRow(0)
|
||||
rows = min(7, self._popup.count())
|
||||
h = 8 + rows * 22
|
||||
self._popup.resize(max(280, self.width()), h)
|
||||
tl = self.mapToGlobal(self.rect().topLeft())
|
||||
self._popup.move(tl.x(), tl.y() - h - 2)
|
||||
self._popup.show()
|
||||
|
||||
def _add_row(self, label: str, replacement: str, tip: str) -> None:
|
||||
it = QListWidgetItem(label)
|
||||
it.setData(Qt.UserRole, replacement)
|
||||
it.setToolTip(tip)
|
||||
self._popup.addItem(it)
|
||||
|
||||
def _accept(self) -> None:
|
||||
item = self._popup.currentItem()
|
||||
self._popup.hide()
|
||||
if item is None:
|
||||
return
|
||||
replacement = item.data(Qt.UserRole)
|
||||
tok = _directive_token(self.text(), self.cursorPosition())
|
||||
start = tok[0] if tok else self.cursorPosition()
|
||||
pos = self.cursorPosition()
|
||||
full = self.text()
|
||||
new_text = full[:start] + replacement + full[pos:]
|
||||
self.setText(new_text)
|
||||
self.setCursorPosition(start + len(replacement))
|
||||
self.setFocus()
|
||||
|
||||
def focusOutEvent(self, e): # noqa: N802
|
||||
if not self._popup.underMouse():
|
||||
self._popup.hide()
|
||||
super().focusOutEvent(e)
|
||||
|
||||
def keyPressEvent(self, e): # noqa: N802
|
||||
if self._popup.isVisible():
|
||||
k = e.key()
|
||||
n = self._popup.count()
|
||||
if k in (Qt.Key_Down, Qt.Key_Up) and n:
|
||||
step = 1 if k == Qt.Key_Down else -1
|
||||
self._popup.setCurrentRow((self._popup.currentRow() + step) % n)
|
||||
return
|
||||
if k in (Qt.Key_Tab,):
|
||||
self._accept()
|
||||
return
|
||||
if k == Qt.Key_Escape:
|
||||
self._popup.hide()
|
||||
return
|
||||
if k in (Qt.Key_Return, Qt.Key_Enter):
|
||||
self._accept()
|
||||
return
|
||||
if e.key() in (Qt.Key_Return, Qt.Key_Enter):
|
||||
self.submit.emit()
|
||||
return
|
||||
super().keyPressEvent(e)
|
||||
|
||||
|
||||
class Co4ETab(QWidget):
|
||||
status_message = Signal(str)
|
||||
|
||||
@@ -546,38 +406,31 @@ class Co4ETab(QWidget):
|
||||
col.addWidget(self._section("co4e.tab_workflows", wf_body, self.wf_new_btn), 3)
|
||||
|
||||
# --- AGENTS ------------------------------------------------------
|
||||
self.ag_new_btn = QPushButton(tr("co4e.new")); self.ag_new_btn.setIcon(icon("plus"))
|
||||
self.ag_new_btn.setToolTip(tr("co4e.tt_new_agent"))
|
||||
self.ag_new_btn.setObjectName("co4eSectionAction")
|
||||
self.ag_new_btn.setFlat(True)
|
||||
self.ag_new_btn.setCursor(Qt.PointingHandCursor)
|
||||
# Widget cua khu vuc nay da doi sang AgentListPanel (xem
|
||||
# presentation/co4e/agent_list_panel.py); o day chi con giu
|
||||
# ag_new_btn/agent_list/ag_edit_btn/ag_del_btn nhu 4 ten thuoc tinh cu
|
||||
# va tu noi signal - dung nguyen tac panel khong tu wire, Co4ETab moi
|
||||
# biet _new_agent/_edit_agent/_delete_agent.
|
||||
self._agent_panel = AgentListPanel()
|
||||
self.ag_new_btn = self._agent_panel.new_btn
|
||||
self.ag_new_btn.clicked.connect(self._new_agent)
|
||||
ag_body = QWidget(); al = QVBoxLayout(ag_body)
|
||||
al.setContentsMargins(0, 0, 0, 0); al.setSpacing(4)
|
||||
self.agent_list = _PaletteList()
|
||||
al.addWidget(self.agent_list, 1)
|
||||
ag_btns = QHBoxLayout(); ag_btns.setSpacing(4)
|
||||
# Edit/delete act on the selected row, so they stay with the list.
|
||||
self.ag_edit_btn = self._icon_btn("edit", "co4e.tt_edit_agent", self._edit_agent)
|
||||
self.ag_del_btn = self._icon_btn("trash", "co4e.tt_del_agent", self._delete_agent)
|
||||
ag_btns.addWidget(self.ag_edit_btn)
|
||||
ag_btns.addWidget(self.ag_del_btn)
|
||||
ag_btns.addStretch(1)
|
||||
al.addLayout(ag_btns)
|
||||
col.addWidget(self._section("co4e.tab_agents", ag_body, self.ag_new_btn), 3)
|
||||
self.agent_list = self._agent_panel.list_widget
|
||||
self.ag_edit_btn = self._agent_panel.edit_btn
|
||||
self.ag_edit_btn.clicked.connect(self._edit_agent)
|
||||
self.ag_del_btn = self._agent_panel.del_btn
|
||||
self.ag_del_btn.clicked.connect(self._delete_agent)
|
||||
col.addWidget(self._section("co4e.tab_agents", self._agent_panel, self.ag_new_btn), 3)
|
||||
|
||||
# --- SKILLS ------------------------------------------------------
|
||||
self.sk_manage_btn = QPushButton(tr("co4e.manage_skills"))
|
||||
self.sk_manage_btn.setToolTip(tr("co4e.tt_manage_skills"))
|
||||
self.sk_manage_btn.setObjectName("co4eSectionAction")
|
||||
self.sk_manage_btn.setFlat(True)
|
||||
self.sk_manage_btn.setCursor(Qt.PointingHandCursor)
|
||||
# Widget cua khu vuc nay da doi sang SkillsListPanel (xem
|
||||
# presentation/co4e/skills_list_panel.py); o day chi con giu
|
||||
# sk_manage_btn/skill_list nhu 2 ten thuoc tinh cu va tu noi signal -
|
||||
# dung nguyen tac panel khong tu wire, Co4ETab moi biet _manage_skills.
|
||||
self._skills_panel = SkillsListPanel()
|
||||
self.sk_manage_btn = self._skills_panel.manage_btn
|
||||
self.sk_manage_btn.clicked.connect(self._manage_skills)
|
||||
sk_body = QWidget(); sl = QVBoxLayout(sk_body)
|
||||
sl.setContentsMargins(0, 0, 0, 0); sl.setSpacing(4)
|
||||
self.skill_list = _PaletteList()
|
||||
sl.addWidget(self.skill_list, 1)
|
||||
col.addWidget(self._section("co4e.tab_skills", sk_body, self.sk_manage_btn), 2)
|
||||
self.skill_list = self._skills_panel.list_widget
|
||||
col.addWidget(self._section("co4e.tab_skills", self._skills_panel, self.sk_manage_btn), 2)
|
||||
|
||||
# --- RUNS --------------------------------------------------------
|
||||
# A short, always-visible view of the same runs the Flow Status page
|
||||
@@ -874,63 +727,32 @@ class Co4ETab(QWidget):
|
||||
def _build_runs_page(self) -> QWidget:
|
||||
"""The pinned 'Runs' tab: a table of every flow run (name · status · steps
|
||||
done/total · creator · created) for tracking. Double-click a run to open
|
||||
that flow's tab with its live status."""
|
||||
w = QWidget()
|
||||
v = QVBoxLayout(w)
|
||||
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.runs_back_btn = QPushButton(tr("co4e.back_to_flow"))
|
||||
self.runs_back_btn.setIcon(icon("chevron-left"))
|
||||
self.runs_back_btn.setToolTip(tr("co4e.tt_back_to_flow"))
|
||||
that flow's tab with its live status.
|
||||
|
||||
Widget construction lives in ``RunsPagePanel`` (presentation/co4e/
|
||||
co4e_run_control_widget.py); this method just wires the panel's public
|
||||
attributes to the handler methods that know about ``self`` (``_show_runs``,
|
||||
``_stop_selected_run``, ...) — the panel itself stays ignorant of ``Co4ETab``.
|
||||
"""
|
||||
panel = RunsPagePanel()
|
||||
self.runs_back_btn = panel.back_btn
|
||||
self.runs_back_btn.clicked.connect(lambda: self._show_runs(False))
|
||||
hdr.addWidget(self.runs_back_btn)
|
||||
self.runs_title = QLabel(tr("co4e.running_flows"))
|
||||
self.runs_title.setObjectName("hint")
|
||||
hdr.addWidget(self.runs_title)
|
||||
# Show + open the workspace folder where flow outputs land (below the tab,
|
||||
# next to the title) so the files a flow produced are easy to find.
|
||||
self.ws_folder_btn = QPushButton()
|
||||
self.ws_folder_btn.setIcon(icon("folder"))
|
||||
self.ws_folder_btn.setFlat(True)
|
||||
self.ws_folder_btn.setCursor(Qt.PointingHandCursor)
|
||||
self.runs_title = panel.title_label
|
||||
self.ws_folder_btn = panel.ws_folder_btn
|
||||
self.ws_folder_btn.clicked.connect(self._open_workspace_folder)
|
||||
self._refresh_ws_folder_btn()
|
||||
hdr.addWidget(self.ws_folder_btn)
|
||||
hdr.addStretch(1)
|
||||
self.run_stop_btn = QPushButton(tr("co4e.stop"))
|
||||
self.run_stop_btn.setIcon(icon("stop"))
|
||||
self.run_stop_btn.setObjectName("danger")
|
||||
self.run_stop_btn.setToolTip(tr("co4e.tt_stop_run"))
|
||||
self.run_stop_btn = panel.stop_btn
|
||||
self.run_stop_btn.clicked.connect(self._stop_selected_run)
|
||||
self.run_rename_btn = QPushButton(tr("co4e.rename_run"))
|
||||
self.run_rename_btn.setIcon(icon("edit"))
|
||||
self.run_rename_btn.setToolTip(tr("co4e.tt_rename_run"))
|
||||
self.run_rename_btn = panel.rename_btn
|
||||
self.run_rename_btn.clicked.connect(self._rename_selected_run)
|
||||
self.run_del_btn = QPushButton(tr("co4e.delete_run"))
|
||||
self.run_del_btn.setIcon(icon("trash"))
|
||||
self.run_del_btn.setToolTip(tr("co4e.tt_delete_run"))
|
||||
self.run_del_btn = panel.del_btn
|
||||
self.run_del_btn.clicked.connect(self._delete_selected_run)
|
||||
self.run_clear_btn = QPushButton(tr("co4e.clear_done"))
|
||||
self.run_clear_btn.setToolTip(tr("co4e.tt_clear_runs"))
|
||||
self.run_clear_btn = panel.clear_btn
|
||||
self.run_clear_btn.clicked.connect(lambda: self.manager.clear_finished())
|
||||
hdr.addWidget(self.run_stop_btn)
|
||||
hdr.addWidget(self.run_rename_btn)
|
||||
hdr.addWidget(self.run_del_btn)
|
||||
hdr.addWidget(self.run_clear_btn)
|
||||
v.addLayout(hdr)
|
||||
self.runs_table = QTableWidget(0, 5)
|
||||
self.runs_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||
self.runs_table.verticalHeader().setVisible(False)
|
||||
self.runs_table.setEditTriggers(QTableWidget.NoEditTriggers)
|
||||
self.runs_table.setSelectionBehavior(QTableWidget.SelectRows)
|
||||
self.runs_table.setToolTip(tr("co4e.tt_runs_list"))
|
||||
self.runs_table = panel.table
|
||||
self.runs_table.itemDoubleClicked.connect(self._open_run_from_table)
|
||||
# Right-click a run → Open / Delete (delete a single old run from history).
|
||||
self.runs_table.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.runs_table.customContextMenuRequested.connect(self._runs_context_menu)
|
||||
v.addWidget(self.runs_table, 1)
|
||||
return w
|
||||
return panel
|
||||
|
||||
def _wrap_config(self) -> QWidget:
|
||||
"""Wrap the step-config panel with a header that has an expand/collapse
|
||||
@@ -1063,69 +885,36 @@ class Co4ETab(QWidget):
|
||||
self.canvas.add_overlay(bar)
|
||||
|
||||
def _build_chat(self) -> QWidget:
|
||||
w = QWidget()
|
||||
self._chat_widget = w
|
||||
lay = QVBoxLayout(w)
|
||||
lay.setContentsMargins(0, 0, 0, 0)
|
||||
lay.setSpacing(0)
|
||||
# "Messages" header at the TOP, above the chat box. Toggling it shows or
|
||||
# hides the WHOLE chat box (message list + composer) below it.
|
||||
self._mhdr = QWidget(); self._mhdr.setObjectName("msgHeader")
|
||||
mh = QHBoxLayout(self._mhdr); 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.chat_toggle_btn = QPushButton()
|
||||
self.chat_toggle_btn.setObjectName("msgToggle")
|
||||
self.chat_toggle_btn.setFlat(True)
|
||||
self.chat_toggle_btn.setIcon(icon("chevron-up")) # collapsed → points up (click to expand)
|
||||
self.chat_toggle_btn.setFixedSize(22, 22)
|
||||
"""Widget construction lives in ``ChatPanel`` (presentation/co4e/
|
||||
co4e_chat_view.py); this method just wires the panel's public
|
||||
attributes to the handler methods that know about ``self``
|
||||
(``_toggle_messages``, ``_chat_send``) and keeps the state that is
|
||||
NOT part of the panel's own construction (``_flow_logs`` — per-flow
|
||||
ChatView dict, ``_co4e_routed_provider`` — routing override, and
|
||||
``_vsplit_sizes``/``_msgs_collapsed`` — used by ``_toggle_messages``
|
||||
below to restore/collapse the splitter) — the panel itself stays
|
||||
ignorant of ``Co4ETab``.
|
||||
"""
|
||||
panel = ChatPanel(self.ctx)
|
||||
self._chat_widget = panel
|
||||
self.msgs_icon = panel.msgs_icon
|
||||
self.msgs_title = panel.msgs_title
|
||||
self.chat_toggle_btn = panel.chat_toggle_btn
|
||||
self.chat_toggle_btn.clicked.connect(self._toggle_messages)
|
||||
mh.addWidget(self.msgs_icon)
|
||||
mh.addWidget(self.msgs_title)
|
||||
mh.addStretch(1)
|
||||
mh.addWidget(self.chat_toggle_btn)
|
||||
lay.addWidget(self._mhdr) # header on top
|
||||
# Point-conversation (message bubbles) like Cowork, not a flat textbox.
|
||||
# ONE ChatView PER FLOW (keyed by workflow id) inside a stack, so each flow
|
||||
# tab has its OWN separate conversation and they never bleed into each other.
|
||||
from PySide6.QtWidgets import QStackedWidget
|
||||
self.chat_stack = QStackedWidget()
|
||||
self._mhdr = panel.header
|
||||
self.chat_stack = panel.chat_stack
|
||||
self._flow_logs: Dict[str, ChatView] = {}
|
||||
lay.addWidget(self.chat_stack, 1)
|
||||
self.chat_input_row = QWidget()
|
||||
crow = QVBoxLayout(self.chat_input_row)
|
||||
crow.setContentsMargins(0, 4, 0, 0); crow.setSpacing(3)
|
||||
# Running conversation token/cost TOTAL for this flow (↓in ↑out ▤ctx
|
||||
# $cost) at the bottom, exactly like Cowork's conversation total.
|
||||
self._usage_total_lbl = QLabel("")
|
||||
self._usage_total_lbl.setObjectName("hint")
|
||||
self._usage_total_lbl.setStyleSheet(
|
||||
f"color: {current_palette().text_faint}; font-size: 11px;")
|
||||
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"))
|
||||
self.chat_input_row = panel.chat_input_row
|
||||
self._usage_total_lbl = panel.usage_total_lbl
|
||||
self.chat_input = panel.chat_input
|
||||
self.chat_input.submit.connect(self._chat_send)
|
||||
self.chat_send_btn = QPushButton(tr("co4e.send")); self.chat_send_btn.setIcon(icon("send"))
|
||||
self.chat_send_btn = panel.chat_send_btn
|
||||
self.chat_send_btn.clicked.connect(self._chat_send)
|
||||
row.addWidget(self.chat_input, 1)
|
||||
# Off/Auto/Manual routing toggle for Co4E (surface key "co4e").
|
||||
from .routing_toggle import RoutingToggle
|
||||
self.co4e_routing_toggle = RoutingToggle(self.ctx, "co4e")
|
||||
self.co4e_routing_toggle = panel.co4e_routing_toggle
|
||||
self._co4e_routed_provider = None # routing provider override for the next turn
|
||||
row.addWidget(self.co4e_routing_toggle)
|
||||
row.addWidget(self.chat_send_btn)
|
||||
crow.addWidget(_inp)
|
||||
lay.addWidget(self.chat_input_row)
|
||||
# Default = COLLAPSED: only the "Messages" header shows; the chat box is
|
||||
# hidden and the canvas gets the room until the user expands it.
|
||||
self._vsplit_sizes = [540, 220] # sizes to restore when expanded
|
||||
self._msgs_collapsed = True
|
||||
self.chat_stack.hide()
|
||||
self.chat_input_row.hide()
|
||||
self.chat_toggle_btn.setToolTip(tr("co4e.tt_expand_msgs"))
|
||||
w.setMaximumHeight(self._mhdr.sizeHint().height() + 6)
|
||||
return w
|
||||
return panel
|
||||
|
||||
def _toggle_messages(self) -> None:
|
||||
"""Show/hide the WHOLE chat box (message list + composer) below the
|
||||
|
||||
Reference in New Issue
Block a user