Co4E step config as collapsible sections; tighter Kanban lanes; Output panel fills its column
CI / test (pull_request) Canceled after 0s
CI / test (pull_request) Canceled after 0s
- co4e_config_panel: StepConfigPanel reorganized into four collapsible, borderless sections (Co ban / Model & Quyen / Skills & Tep / Agent song song) instead of one long form. Agent song song is its own section, shown only for parallel-variant steps. Fixes a QVBoxLayout without a trailing stretch that was handing every section an equal share of the scroll area's leftover height instead of sizing to content. - schedule_task_tab / theme: Kanban board lane gutters and per-lane margins trimmed so the seven lanes read their card content more clearly on a laptop-width window. - chat_panel / widgets: the Output Files list in Cowork/Code no longer stops at a fixed height with empty panel below it - it now fills the column down to the composer. - composer: Attach/Send/Stop buttons match the input box's height and align to its top/bottom edge instead of floating centered beside it. - app.py: the sidebar's "All projects" link reads as a link (italic, accent color) instead of a plain row. - i18n: labels for the three renamed Co4E section headers (VI/EN/JA).
This commit is contained in:
@@ -7,7 +7,7 @@ from pathlib import Path
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, QTimer
|
from PySide6.QtCore import Qt, QTimer
|
||||||
from PySide6.QtGui import QGuiApplication, QIcon
|
from PySide6.QtGui import QColor, QGuiApplication, QIcon
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QStyledItemDelegate,
|
QStyledItemDelegate,
|
||||||
QApplication, QComboBox, QHBoxLayout, QLabel, QMainWindow, QMenu,
|
QApplication, QComboBox, QHBoxLayout, QLabel, QMainWindow, QMenu,
|
||||||
@@ -747,9 +747,14 @@ class MainWindow(QMainWindow):
|
|||||||
it = QTreeWidgetItem([tr("sidebar.empty")])
|
it = QTreeWidgetItem([tr("sidebar.empty")])
|
||||||
it.setDisabled(True)
|
it.setDisabled(True)
|
||||||
tree.addTopLevelItem(it)
|
tree.addTopLevelItem(it)
|
||||||
# The way back to everything the rail cannot show.
|
# The way back to everything the rail cannot show — styled as a link
|
||||||
|
# (italic, accent-colored) so it reads as "go elsewhere", not another row.
|
||||||
more = QTreeWidgetItem([tr("app.nav.all_projects")])
|
more = QTreeWidgetItem([tr("app.nav.all_projects")])
|
||||||
more.setData(0, Qt.UserRole, {"all": True})
|
more.setData(0, Qt.UserRole, {"all": True})
|
||||||
|
more_font = more.font(0)
|
||||||
|
more_font.setItalic(True)
|
||||||
|
more.setFont(0, more_font)
|
||||||
|
more.setForeground(0, QColor(current_palette().accent))
|
||||||
tree.addTopLevelItem(more)
|
tree.addTopLevelItem(more)
|
||||||
tree.blockSignals(blocked)
|
tree.blockSignals(blocked)
|
||||||
self.nav_recents_hdr.setVisible(not self._nav_collapsed)
|
self.nav_recents_hdr.setVisible(not self._nav_collapsed)
|
||||||
|
|||||||
@@ -2765,6 +2765,9 @@ STRINGS: Dict[str, Dict[str, str]] = {
|
|||||||
"ja": "フローとチャット — /agent:<name> または /skill:<name>",
|
"ja": "フローとチャット — /agent:<name> または /skill:<name>",
|
||||||
"vi": "Chat với flow — dùng /agent:<name> hoặc /skill:<name>"},
|
"vi": "Chat với flow — dùng /agent:<name> hoặc /skill:<name>"},
|
||||||
"co4e.send": {"en": "Send", "ja": "送信", "vi": "Gửi"},
|
"co4e.send": {"en": "Send", "ja": "送信", "vi": "Gửi"},
|
||||||
|
"co4e.tab_basic": {"en": "Basic", "ja": "基本", "vi": "Cơ bản"},
|
||||||
|
"co4e.tab_model_perm": {"en": "Model & Permission", "ja": "モデルと権限", "vi": "Model & Quyền"},
|
||||||
|
"co4e.tab_skills_files": {"en": "Skills & Files", "ja": "スキルとファイル", "vi": "Skills & Tệp"},
|
||||||
"co4e.f_label": {"en": "Label", "ja": "ラベル", "vi": "Nhãn"},
|
"co4e.f_label": {"en": "Label", "ja": "ラベル", "vi": "Nhãn"},
|
||||||
"co4e.f_role": {"en": "Role", "ja": "ロール", "vi": "Vai trò"},
|
"co4e.f_role": {"en": "Role", "ja": "ロール", "vi": "Vai trò"},
|
||||||
"co4e.f_name": {"en": "Name", "ja": "名前", "vi": "Tên"},
|
"co4e.f_name": {"en": "Name", "ja": "名前", "vi": "Tên"},
|
||||||
|
|||||||
@@ -641,6 +641,10 @@ QTreeView::item:selected, QListView::item:selected, QTableView::item:selected {
|
|||||||
cell on top of the selection tint above — visible as a stray light border
|
cell on top of the selection tint above — visible as a stray light border
|
||||||
on a click. The selection tint already marks "current row"; drop the rect. */
|
on a click. The selection tint already marks "current row"; drop the rect. */
|
||||||
QTreeView::item:focus, QListView::item:focus, QTableView::item:focus { outline: none; border: none; }
|
QTreeView::item:focus, QListView::item:focus, QTableView::item:focus { outline: none; border: none; }
|
||||||
|
/* Kanban lanes (Schedule Task): seven lanes share the board width, so a card's
|
||||||
|
own inset competes with the other six for space the same way the inter-lane
|
||||||
|
gap did — trimmed to match. */
|
||||||
|
QListWidget#kanbanLane::item { padding: 3px 2px; }
|
||||||
QHeaderView::section {
|
QHeaderView::section {
|
||||||
background: $bg; color: $text_muted; border: none;
|
background: $bg; color: $text_muted; border: none;
|
||||||
border-bottom: 1px solid $border; padding: 7px 6px; font-weight: 600;
|
border-bottom: 1px solid $border; padding: 7px 6px; font-weight: 600;
|
||||||
|
|||||||
+6
-3
@@ -214,7 +214,11 @@ class ChatPanel(QWidget):
|
|||||||
# Right sidebar: Output files only (see below — Input is tracked but
|
# Right sidebar: Output files only (see below — Input is tracked but
|
||||||
# not shown).
|
# not shown).
|
||||||
self.input_section = CollapsibleSection(tr("widgets.input_files"))
|
self.input_section = CollapsibleSection(tr("widgets.input_files"))
|
||||||
self.output_section = CollapsibleSection(tr("widgets.output_files").upper())
|
# No cap: this section owns the whole right panel (its header is
|
||||||
|
# hoisted into io_hdr below), so the list should fill the space down
|
||||||
|
# to the composer instead of stopping at a fixed height with empty
|
||||||
|
# panel below it.
|
||||||
|
self.output_section = CollapsibleSection(tr("widgets.output_files").upper(), max_height=None)
|
||||||
# Input files are NOT shown in Cowork's UI anymore — but they're still
|
# Input files are NOT shown in Cowork's UI anymore — but they're still
|
||||||
# fully tracked (add/remove/paths()) exactly as before, since that list
|
# fully tracked (add/remove/paths()) exactly as before, since that list
|
||||||
# is what gets written into the conversation's own "inputs" field on
|
# is what gets written into the conversation's own "inputs" field on
|
||||||
@@ -273,8 +277,7 @@ class ChatPanel(QWidget):
|
|||||||
bl = QVBoxLayout(bl_host)
|
bl = QVBoxLayout(bl_host)
|
||||||
bl.setContentsMargins(0, 0, 0, 0)
|
bl.setContentsMargins(0, 0, 0, 0)
|
||||||
bl.setSpacing(4)
|
bl.setSpacing(4)
|
||||||
bl.addWidget(self.output_section) # Output only — Input is tracked but hidden
|
bl.addWidget(self.output_section, 1) # Output only — Input is tracked but hidden; fills down to the composer
|
||||||
bl.addStretch(1)
|
|
||||||
iol.addWidget(bl_host, 1)
|
iol.addWidget(bl_host, 1)
|
||||||
|
|
||||||
# Collapsing shrinks the panel to a thin clickable line (not hidden).
|
# Collapsing shrinks the panel to a thin clickable line (not hidden).
|
||||||
|
|||||||
+155
-23
@@ -11,18 +11,124 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, Signal
|
from PySide6.QtCore import Qt, QEasingCurve, QPropertyAnimation, Signal
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QCheckBox, QComboBox, QFormLayout, QHBoxLayout, QInputDialog, QLabel,
|
QCheckBox, QComboBox, QFormLayout, QHBoxLayout, QInputDialog,
|
||||||
QLineEdit, QListWidget, QListWidgetItem, QPlainTextEdit, QPushButton,
|
QLabel, QLineEdit, QListWidget, QListWidgetItem, QPlainTextEdit,
|
||||||
QScrollArea, QSpinBox, QVBoxLayout, QWidget,
|
QPushButton, QScrollArea, QSpinBox, QVBoxLayout, QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..config import PROVIDER_LABELS
|
from ..config import PROVIDER_LABELS
|
||||||
from ..core.co4e import PERMISSION_PRESETS, Step, SubAgent
|
from ..core.co4e import PERMISSION_PRESETS, Step, SubAgent
|
||||||
from ..i18n import tr
|
from ..i18n import tr
|
||||||
|
from ..theme import current_palette
|
||||||
from .icons import icon, icon_picker_combo
|
from .icons import icon, icon_picker_combo
|
||||||
|
|
||||||
|
_SECTION_ANIM_MS = 180
|
||||||
|
|
||||||
|
|
||||||
|
class _SectionHeader(QLabel):
|
||||||
|
"""A clickable label — a QPushButton's own style chrome (border, native
|
||||||
|
button margin, focus rect) always leaves a taller minimum height than a
|
||||||
|
plain label, even once its QSS padding is zeroed out, so the header that
|
||||||
|
needs to sit tight against its neighbours is a label, not a button."""
|
||||||
|
|
||||||
|
clicked = Signal()
|
||||||
|
|
||||||
|
def mousePressEvent(self, event) -> None: # noqa: N802
|
||||||
|
if event.button() == Qt.LeftButton:
|
||||||
|
self.clicked.emit()
|
||||||
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
|
def showEvent(self, event) -> None: # noqa: N802
|
||||||
|
# fontMetrics() at construction time (before this label is ever part
|
||||||
|
# of a shown top-level window) reflects the QSS font-size only if the
|
||||||
|
# style has fully polished by then — on the very FIRST paint of the
|
||||||
|
# Co4E screen it sometimes hasn't, so the fixed height computed in
|
||||||
|
# _add_section is briefly wrong (too tall) until something else
|
||||||
|
# triggers a relayout. Recomputing here, every time the label
|
||||||
|
# actually becomes visible, means the first paint is never stale.
|
||||||
|
self.setFixedHeight(self.fontMetrics().height())
|
||||||
|
super().showEvent(event)
|
||||||
|
|
||||||
|
|
||||||
|
def _add_section(outer: QVBoxLayout, title: 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
|
||||||
|
border/background/box — the ▶/▼ marker and the heading text are the only
|
||||||
|
things separating one group from the next; opening one never closes
|
||||||
|
another (not an accordion, not a tab bar). Returns ``(form, card)``: add
|
||||||
|
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."""
|
||||||
|
p = current_palette()
|
||||||
|
card = QWidget()
|
||||||
|
card_lay = QVBoxLayout(card)
|
||||||
|
card_lay.setContentsMargins(0, 0, 0, 0)
|
||||||
|
card_lay.setSpacing(0)
|
||||||
|
|
||||||
|
header = _SectionHeader()
|
||||||
|
header.setCursor(Qt.PointingHandCursor)
|
||||||
|
header.setStyleSheet(f"font-weight:400; font-size:13px; color:{p.text}; padding:0; margin:0;")
|
||||||
|
header.setContentsMargins(0, 0, 0, 0)
|
||||||
|
# QSS font-size only lands on the widget's actual QFont (and therefore
|
||||||
|
# its fontMetrics()) once the style sheet is polished — ensurePolished()
|
||||||
|
# forces that now, so the fixed height below is computed from the 12px
|
||||||
|
# font just set above, not the default one this label was constructed
|
||||||
|
# with. A label's natural sizeHint still reserves font leading above/
|
||||||
|
# below the glyphs on top of the (now zeroed) QSS padding — pinning the
|
||||||
|
# height to the text's actual cap-to-baseline span is what closes that
|
||||||
|
# last gap without clipping the ▶ glyph, the title, or Vietnamese
|
||||||
|
# diacritics.
|
||||||
|
header.ensurePolished()
|
||||||
|
header.setFixedHeight(header.fontMetrics().height())
|
||||||
|
header.setText(f"▶ {title}")
|
||||||
|
card_lay.addWidget(header)
|
||||||
|
|
||||||
|
body = QWidget()
|
||||||
|
body.setVisible(False)
|
||||||
|
body.setMaximumHeight(0)
|
||||||
|
form = QFormLayout(body)
|
||||||
|
form.setContentsMargins(0, 6, 0, 0)
|
||||||
|
card_lay.addWidget(body)
|
||||||
|
|
||||||
|
anim = QPropertyAnimation(body, b"maximumHeight", body)
|
||||||
|
anim.setDuration(_SECTION_ANIM_MS)
|
||||||
|
anim.setEasingCurve(QEasingCurve.InOutCubic)
|
||||||
|
|
||||||
|
is_open = False
|
||||||
|
|
||||||
|
def _on_finished() -> None:
|
||||||
|
if is_open:
|
||||||
|
# Uncapped once open, so switching to a step whose fields make
|
||||||
|
# this section taller/shorter (e.g. a parallel node's sub-agent
|
||||||
|
# list appearing) is never clipped by the height this animation
|
||||||
|
# last landed on.
|
||||||
|
body.setMaximumHeight(16_777_215)
|
||||||
|
else:
|
||||||
|
body.setVisible(False)
|
||||||
|
anim.finished.connect(_on_finished)
|
||||||
|
|
||||||
|
def _toggle() -> None:
|
||||||
|
nonlocal is_open
|
||||||
|
is_open = not is_open
|
||||||
|
header.setText(f"{'▼' if is_open else '▶'} {title}")
|
||||||
|
anim.stop()
|
||||||
|
if is_open:
|
||||||
|
body.setVisible(True)
|
||||||
|
anim.setStartValue(body.height())
|
||||||
|
anim.setEndValue(body.sizeHint().height())
|
||||||
|
else:
|
||||||
|
anim.setStartValue(body.height())
|
||||||
|
anim.setEndValue(0)
|
||||||
|
anim.start()
|
||||||
|
header.clicked.connect(_toggle)
|
||||||
|
|
||||||
|
outer.addWidget(card)
|
||||||
|
return form, card
|
||||||
|
|
||||||
|
|
||||||
class StepConfigPanel(QScrollArea):
|
class StepConfigPanel(QScrollArea):
|
||||||
changed = Signal() # any field edited → repaint node + autosave
|
changed = Signal() # any field edited → repaint node + autosave
|
||||||
@@ -39,7 +145,16 @@ class StepConfigPanel(QScrollArea):
|
|||||||
self.setWidgetResizable(True)
|
self.setWidgetResizable(True)
|
||||||
host = QWidget()
|
host = QWidget()
|
||||||
self.setWidget(host)
|
self.setWidget(host)
|
||||||
form = QFormLayout(host)
|
outer = QVBoxLayout(host)
|
||||||
|
outer.setSpacing(1)
|
||||||
|
|
||||||
|
# Grouped sections stacked on one scrolling page — same fields as
|
||||||
|
# before, grouped by what they're for: identity, execution
|
||||||
|
# (model/permission), and the extra resources fed to the step
|
||||||
|
# (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"))
|
||||||
|
|
||||||
self.label_edit = QLineEdit()
|
self.label_edit = QLineEdit()
|
||||||
self.label_edit.textChanged.connect(self._on_edit)
|
self.label_edit.textChanged.connect(self._on_edit)
|
||||||
@@ -80,6 +195,8 @@ class StepConfigPanel(QScrollArea):
|
|||||||
self.context_edit.textChanged.connect(self._on_edit)
|
self.context_edit.textChanged.connect(self._on_edit)
|
||||||
form.addRow(tr("co4e.f_context"), self.context_edit)
|
form.addRow(tr("co4e.f_context"), self.context_edit)
|
||||||
|
|
||||||
|
form2, _model_card = _add_section(outer, tr("co4e.tab_model_perm"))
|
||||||
|
|
||||||
model_row = QHBoxLayout()
|
model_row = QHBoxLayout()
|
||||||
self.model_combo = QComboBox()
|
self.model_combo = QComboBox()
|
||||||
self.model_combo.setEditable(True)
|
self.model_combo.setEditable(True)
|
||||||
@@ -92,13 +209,13 @@ class StepConfigPanel(QScrollArea):
|
|||||||
model_row.addWidget(self.model_combo, 1)
|
model_row.addWidget(self.model_combo, 1)
|
||||||
model_row.addWidget(self.load_models_btn)
|
model_row.addWidget(self.load_models_btn)
|
||||||
mrow = QWidget(); mrow.setLayout(model_row)
|
mrow = QWidget(); mrow.setLayout(model_row)
|
||||||
form.addRow(tr("co4e.f_model"), mrow)
|
form2.addRow(tr("co4e.f_model"), mrow)
|
||||||
|
|
||||||
self.perm_combo = QComboBox()
|
self.perm_combo = QComboBox()
|
||||||
for preset in PERMISSION_PRESETS:
|
for preset in PERMISSION_PRESETS:
|
||||||
self.perm_combo.addItem(tr(f"co4e.perm.{preset}"), preset)
|
self.perm_combo.addItem(tr(f"co4e.perm.{preset}"), preset)
|
||||||
self.perm_combo.currentIndexChanged.connect(self._on_edit)
|
self.perm_combo.currentIndexChanged.connect(self._on_edit)
|
||||||
form.addRow(tr("co4e.f_permission"), self.perm_combo)
|
form2.addRow(tr("co4e.f_permission"), self.perm_combo)
|
||||||
|
|
||||||
verify_row = QHBoxLayout()
|
verify_row = QHBoxLayout()
|
||||||
self.verify_chk = QCheckBox(tr("co4e.f_self_verify"))
|
self.verify_chk = QCheckBox(tr("co4e.f_self_verify"))
|
||||||
@@ -111,13 +228,15 @@ class StepConfigPanel(QScrollArea):
|
|||||||
verify_row.addWidget(self.rounds_spin)
|
verify_row.addWidget(self.rounds_spin)
|
||||||
verify_row.addStretch(1)
|
verify_row.addStretch(1)
|
||||||
vrow = QWidget(); vrow.setLayout(verify_row)
|
vrow = QWidget(); vrow.setLayout(verify_row)
|
||||||
form.addRow("", vrow)
|
form2.addRow("", vrow)
|
||||||
|
|
||||||
|
form3, _skills_card = _add_section(outer, tr("co4e.tab_skills_files"))
|
||||||
|
|
||||||
# Skills checklist (registry skills)
|
# Skills checklist (registry skills)
|
||||||
self.skills_list = QListWidget()
|
self.skills_list = QListWidget()
|
||||||
self.skills_list.setMaximumHeight(110)
|
self.skills_list.setMaximumHeight(110)
|
||||||
self.skills_list.itemChanged.connect(self._on_edit)
|
self.skills_list.itemChanged.connect(self._on_edit)
|
||||||
form.addRow(tr("co4e.f_skills"), self.skills_list)
|
form3.addRow(tr("co4e.f_skills"), self.skills_list)
|
||||||
|
|
||||||
# Attachments — files whose extracted text is fed to this step at run time.
|
# Attachments — files whose extracted text is fed to this step at run time.
|
||||||
self.attach_list = QListWidget()
|
self.attach_list = QListWidget()
|
||||||
@@ -133,11 +252,15 @@ class StepConfigPanel(QScrollArea):
|
|||||||
att_btns.addWidget(self.attach_del_btn)
|
att_btns.addWidget(self.attach_del_btn)
|
||||||
att_btns.addStretch(1)
|
att_btns.addStretch(1)
|
||||||
abtn = QWidget(); abtn.setLayout(att_btns)
|
abtn = QWidget(); abtn.setLayout(att_btns)
|
||||||
form.addRow(tr("co4e.f_attachments"), self.attach_list)
|
form3.addRow(tr("co4e.f_attachments"), self.attach_list)
|
||||||
form.addRow("", abtn)
|
form3.addRow("", abtn)
|
||||||
|
|
||||||
# Parallel sub-agents (only shown for parallel nodes)
|
# Parallel sub-agents get their OWN section — same header style as
|
||||||
self.parallel_label = QLabel(tr("co4e.f_subagents"))
|
# Cơ bản/Model & Quyền/Skills & Tệp — rather than a row buried inside
|
||||||
|
# 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"))
|
||||||
self.sub_list = QListWidget()
|
self.sub_list = QListWidget()
|
||||||
self.sub_list.setMaximumHeight(90)
|
self.sub_list.setMaximumHeight(90)
|
||||||
self.sub_list.itemDoubleClicked.connect(self._edit_subagent) # re-pick agent
|
self.sub_list.itemDoubleClicked.connect(self._edit_subagent) # re-pick agent
|
||||||
@@ -152,10 +275,11 @@ class StepConfigPanel(QScrollArea):
|
|||||||
sub_btns.addWidget(self.sub_del_btn)
|
sub_btns.addWidget(self.sub_del_btn)
|
||||||
sub_btns.addStretch(1)
|
sub_btns.addStretch(1)
|
||||||
sbtn = QWidget(); sbtn.setLayout(sub_btns)
|
sbtn = QWidget(); sbtn.setLayout(sub_btns)
|
||||||
form.addRow(self.parallel_label, self.sub_list)
|
form4.addRow(self.sub_list)
|
||||||
form.addRow("", sbtn)
|
form4.addRow("", sbtn)
|
||||||
|
|
||||||
# Footer actions — one compact row (Run · Run from here · Delete).
|
# 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 = QPushButton(tr("co4e.run"))
|
||||||
self.run_btn.setIcon(icon("play"))
|
self.run_btn.setIcon(icon("play"))
|
||||||
self.run_btn.setToolTip(tr("co4e.run_this_step"))
|
self.run_btn.setToolTip(tr("co4e.run_this_step"))
|
||||||
@@ -170,12 +294,21 @@ class StepConfigPanel(QScrollArea):
|
|||||||
self.del_btn.setFixedWidth(38)
|
self.del_btn.setFixedWidth(38)
|
||||||
self.del_btn.clicked.connect(lambda: self.delete_node.emit(self._node_id))
|
self.del_btn.clicked.connect(lambda: self.delete_node.emit(self._node_id))
|
||||||
foot = QHBoxLayout()
|
foot = QHBoxLayout()
|
||||||
foot.setContentsMargins(0, 0, 0, 0)
|
|
||||||
foot.addWidget(self.run_btn, 1)
|
foot.addWidget(self.run_btn, 1)
|
||||||
foot.addWidget(self.run_from_btn, 1)
|
foot.addWidget(self.run_from_btn, 1)
|
||||||
foot.addWidget(self.del_btn)
|
foot.addWidget(self.del_btn)
|
||||||
foot_w = QWidget(); foot_w.setLayout(foot)
|
foot_w = QWidget(); foot_w.setLayout(foot)
|
||||||
form.addRow("", foot_w)
|
outer.addWidget(foot_w)
|
||||||
|
# Without this, QVBoxLayout hands every child widget an EQUAL share of
|
||||||
|
# whatever extra height the scroll area's viewport has beyond the
|
||||||
|
# content's own sizeHint (setWidgetResizable(True) stretches `host` to
|
||||||
|
# fill it) — each collapsed header's card was measuring a true
|
||||||
|
# sizeHint of ~17px but rendering over 100px taller, and no amount of
|
||||||
|
# margin/padding/spacing on the header itself could touch that: the
|
||||||
|
# surplus was being spent on the cards, not around them. One trailing
|
||||||
|
# stretch absorbs all of it instead, so every section (and the
|
||||||
|
# footer) renders at exactly its own natural height.
|
||||||
|
outer.addStretch(1)
|
||||||
|
|
||||||
self.setEnabled(False)
|
self.setEnabled(False)
|
||||||
|
|
||||||
@@ -209,12 +342,11 @@ class StepConfigPanel(QScrollArea):
|
|||||||
item = QListWidgetItem(_P(path).name)
|
item = QListWidgetItem(_P(path).name)
|
||||||
item.setToolTip(path)
|
item.setToolTip(path)
|
||||||
self.attach_list.addItem(item)
|
self.attach_list.addItem(item)
|
||||||
# parallel sub-agents
|
# parallel sub-agents — the whole "Agent song song" section only
|
||||||
|
# applies to parallel-variant steps, so the entire card (header
|
||||||
|
# included) is hidden for any other step, not just its rows.
|
||||||
is_par = step.is_parallel
|
is_par = step.is_parallel
|
||||||
self.parallel_label.setVisible(is_par)
|
self._parallel_card.setVisible(is_par)
|
||||||
self.sub_list.setVisible(is_par)
|
|
||||||
self.sub_add_btn.setVisible(is_par)
|
|
||||||
self.sub_del_btn.setVisible(is_par)
|
|
||||||
self.sub_list.clear()
|
self.sub_list.clear()
|
||||||
if is_par:
|
if is_par:
|
||||||
for sub in step.sub_agents:
|
for sub in step.sub_agents:
|
||||||
|
|||||||
@@ -452,7 +452,12 @@ class Composer(QWidget):
|
|||||||
self.stop_btn.setObjectName("danger")
|
self.stop_btn.setObjectName("danger")
|
||||||
self.stop_btn.setVisible(False)
|
self.stop_btn.setVisible(False)
|
||||||
self.stop_btn.clicked.connect(self.stop_requested.emit)
|
self.stop_btn.clicked.connect(self.stop_requested.emit)
|
||||||
|
# Attach pinned to the input's top edge, Send (and Stop, once a turn
|
||||||
|
# is running) pinned to its bottom edge — the gap between them is
|
||||||
|
# absorbed by this stretch instead of splitting evenly above/below
|
||||||
|
# the whole button column, which is what centering it did before.
|
||||||
btns.addWidget(self.attach_btn)
|
btns.addWidget(self.attach_btn)
|
||||||
|
btns.addStretch(1)
|
||||||
btns.addWidget(self.send_btn)
|
btns.addWidget(self.send_btn)
|
||||||
btns.addWidget(self.stop_btn)
|
btns.addWidget(self.stop_btn)
|
||||||
row.addLayout(btns)
|
row.addLayout(btns)
|
||||||
|
|||||||
+12
-1
@@ -133,14 +133,25 @@ class ScheduleTaskTab(QWidget):
|
|||||||
board = QWidget()
|
board = QWidget()
|
||||||
scroll.setWidget(board)
|
scroll.setWidget(board)
|
||||||
cols = QHBoxLayout(board)
|
cols = QHBoxLayout(board)
|
||||||
cols.setSpacing(8)
|
# Gutters wide enough to read as a break between lanes without eating
|
||||||
|
# too much of the seven-way split — they still share the board equally
|
||||||
|
# (see _fit_lanes below), so a wider gutter narrows every lane by the
|
||||||
|
# same share automatically; nothing else to compute here.
|
||||||
|
cols.setSpacing(2)
|
||||||
self.columns: Dict[str, _KanbanColumn] = {}
|
self.columns: Dict[str, _KanbanColumn] = {}
|
||||||
self.column_headers: Dict[str, QLabel] = {}
|
self.column_headers: Dict[str, QLabel] = {}
|
||||||
for status in STATUSES:
|
for status in STATUSES:
|
||||||
box = QVBoxLayout()
|
box = QVBoxLayout()
|
||||||
|
# The per-lane holder's own margins were the style's default
|
||||||
|
# (~9px a side) on top of the inter-column gap — with seven lanes
|
||||||
|
# that outweighs the gap itself. Zero it out and let the lane's
|
||||||
|
# header/list fill the width _fit_lanes() hands them.
|
||||||
|
box.setContentsMargins(0, 0, 0, 0)
|
||||||
|
box.setSpacing(2)
|
||||||
head = QLabel()
|
head = QLabel()
|
||||||
head.setStyleSheet("font-weight:600;")
|
head.setStyleSheet("font-weight:600;")
|
||||||
col = _KanbanColumn(status)
|
col = _KanbanColumn(status)
|
||||||
|
col.setObjectName("kanbanLane")
|
||||||
col.task_dropped.connect(self._on_task_dropped)
|
col.task_dropped.connect(self._on_task_dropped)
|
||||||
col.itemDoubleClicked.connect(self._on_double_click)
|
col.itemDoubleClicked.connect(self._on_double_click)
|
||||||
col.setContextMenuPolicy(Qt.CustomContextMenu)
|
col.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
|
|||||||
+10
-3
@@ -739,7 +739,11 @@ class CollapsibleSection(QWidget):
|
|||||||
|
|
||||||
activated = Signal(str) # emits the path of a clicked item
|
activated = Signal(str) # emits the path of a clicked item
|
||||||
|
|
||||||
def __init__(self, title: str, max_height: int = 130):
|
def __init__(self, title: str, max_height: int | None = 130):
|
||||||
|
"""``max_height`` caps the list so it scrolls instead of growing
|
||||||
|
(the default, e.g. for a section sharing space with siblings).
|
||||||
|
``None`` instead lets it expand to fill whatever room its parent
|
||||||
|
layout hands it — for a section that owns the whole panel."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._title = title
|
self._title = title
|
||||||
self._paths: list[str] = []
|
self._paths: list[str] = []
|
||||||
@@ -756,11 +760,14 @@ class CollapsibleSection(QWidget):
|
|||||||
lay.addWidget(self.header)
|
lay.addWidget(self.header)
|
||||||
|
|
||||||
self.list = QListWidget()
|
self.list = QListWidget()
|
||||||
self.list.setMaximumHeight(max_height) # scrolls when longer
|
if max_height is not None:
|
||||||
|
self.list.setMaximumHeight(max_height) # scrolls when longer
|
||||||
|
else:
|
||||||
|
self.list.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
|
||||||
self.list.setVisible(False)
|
self.list.setVisible(False)
|
||||||
self.list.itemActivated.connect(self._emit)
|
self.list.itemActivated.connect(self._emit)
|
||||||
self.list.itemClicked.connect(self._emit)
|
self.list.itemClicked.connect(self._emit)
|
||||||
lay.addWidget(self.list)
|
lay.addWidget(self.list, 1 if max_height is None else 0)
|
||||||
|
|
||||||
self.setVisible(False)
|
self.setVisible(False)
|
||||||
self._update_header()
|
self._update_header()
|
||||||
|
|||||||
Reference in New Issue
Block a user