Files
cowork-local/ui/tools_admin_tab.py
T
2026-08-09 20:12:05 +07:00

201 lines
8.7 KiB
Python

"""Tools — Monitoring tab (Admin) to govern every agent capability.
Two sub-tabs:
* "Tool" — built-in agent tools (read/write/edit files, run commands,
install packages, fetch URLs); toggling one OFF removes it
from the agent's toolset (persisted in ``config.tools_disabled``).
* "Connector" — the full Connectors (MCP / REST API) setup, moved here from
Settings: add/edit/delete CAD/CAE/MS365/Other connectors and
enable/disable each (``ConnectorsPanel``).
"""
from __future__ import annotations
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QCheckBox, QHBoxLayout, QHeaderView, QLabel, QPushButton,
QTableWidget, QTableWidgetItem, QTabWidget, QVBoxLayout, QWidget,
)
from ..core.tools import TOOL_SPECS
from ..core.worker import AgentWorker
from ..i18n import on_language_changed, tr
from ..state import AppContext
from .connectors_panel import ConnectorsPanel
from .icons import icon
def _center_checkbox(checked: bool, on_toggle) -> QWidget:
box = QWidget()
lay = QHBoxLayout(box)
lay.setContentsMargins(0, 0, 0, 0)
lay.setAlignment(Qt.AlignCenter)
chk = QCheckBox()
chk.setChecked(checked)
chk.toggled.connect(on_toggle)
lay.addWidget(chk)
return box
class ToolsAdminTab(QWidget):
def __init__(self, ctx: AppContext):
super().__init__()
self.ctx = ctx
root = QVBoxLayout(self)
self.subtabs = QTabWidget()
root.addWidget(self.subtabs, 1)
# ---- "Tool" sub-tab: built-in agent tools ------------------------
tool_page = QWidget()
tl = QVBoxLayout(tool_page)
self._net_worker = None
self._hint = QLabel()
self._hint.setObjectName("hint")
self._hint.setWordWrap(True)
tl.addWidget(self._hint)
self.table = QTableWidget(0, 3)
self.table.setEditTriggers(QTableWidget.NoEditTriggers)
self.table.verticalHeader().setVisible(False)
# Description is the long column — IT stretches to fill remaining
# width (was Name, leaving Description squeezed into whatever was
# left over); Name/Enabled size to their own content.
self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)
self.table.setWordWrap(True)
tl.addWidget(self.table, 1)
# "Test Internet" self-test lives INSIDE the fetch_url tool row now (see
# refresh) instead of a separate boxed section — persistent widgets so
# they survive table rebuilds.
self.test_internet_btn = QPushButton(tr("settings.test_internet"))
self.test_internet_btn.setIcon(icon("globe"))
self.test_internet_btn.setToolTip(tr("settings.test_internet_tooltip"))
self.test_internet_btn.clicked.connect(self._test_internet)
self.test_internet_status = QLabel("")
self.test_internet_status.setWordWrap(True)
btn_row = QHBoxLayout()
self.refresh_btn = QPushButton()
self.refresh_btn.clicked.connect(self.refresh)
btn_row.addStretch(1)
btn_row.addWidget(self.refresh_btn)
tl.addLayout(btn_row)
# Jira CONNECTION setup lives in the Connector sub-tab now; here the Tool
# list just lets the admin turn the jira_* tools on/off. A pointer note:
self.jira_note = QLabel()
self.jira_note.setObjectName("hint")
self.jira_note.setWordWrap(True)
tl.addWidget(self.jira_note)
self.subtabs.addTab(tool_page, "")
# ---- "Connector" sub-tab: MCP / REST API setup (moved from Settings) --
self.connectors_panel = ConnectorsPanel(ctx)
self.subtabs.addTab(self.connectors_panel, "")
on_language_changed(self._retranslate)
self._retranslate()
# ---- built-in tools table -------------------------------------------------
def refresh(self) -> None:
disabled = set(self.ctx.config.tools_disabled)
specs = list(TOOL_SPECS)
self.table.setRowCount(len(specs))
for r, spec in enumerate(specs):
self.table.setItem(r, 0, QTableWidgetItem(spec.name))
# Full description (was truncated to 80 chars, hiding the rest) —
# word-wraps inside the stretched column; resizeRowToContents
# below grows the row to fit however many lines that takes.
if spec.name == "fetch_url":
# This tool's row carries the live "Test Internet" self-test
# right below its description — no separate boxed section.
self.table.setItem(r, 1, None)
self.table.setCellWidget(r, 1, self._fetch_url_desc_cell(spec))
else:
desc_item = QTableWidgetItem(spec.description)
desc_item.setToolTip(spec.description)
self.table.setItem(r, 1, desc_item)
self.table.setCellWidget(
r, 2, _center_checkbox(spec.name not in disabled,
lambda on, n=spec.name: self._toggle_builtin(n, on)))
# Once ALL rows/columns are populated (so the stretched Description
# column has its real width), grow each row to fit its wrapped text.
self.table.resizeRowsToContents()
def _fetch_url_desc_cell(self, spec) -> QWidget:
cell = QWidget()
cl = QVBoxLayout(cell)
cl.setContentsMargins(6, 4, 6, 4)
cl.setSpacing(4)
desc = QLabel(spec.description)
desc.setWordWrap(True)
desc.setToolTip(spec.description)
cl.addWidget(desc)
net = QWidget()
nl = QHBoxLayout(net)
nl.setContentsMargins(0, 0, 0, 0)
nl.addWidget(self.test_internet_btn)
nl.addWidget(self.test_internet_status, 1)
cl.addWidget(net)
return cell
def _toggle_builtin(self, name: str, enabled: bool) -> None:
self.ctx.config.set_tool_enabled(name, enabled)
# For fetch_url, the Enabled toggle also governs the runtime web-access
# gate (agent_security.allow_url_fetch) — one control for the capability.
if name == "fetch_url":
self.ctx.config.agent_security["allow_url_fetch"] = bool(enabled)
self.ctx.config.save()
def _test_internet(self) -> None:
"""Live-check the app's own outbound HTTPS path and report the concrete
result. Respects the fetch_url toggle: when web access is OFF the agent
cannot reach the internet, so the test reports that instead of probing."""
disabled = ("fetch_url" in self.ctx.config.tools_disabled
or not bool(self.ctx.config.agent_security.get("allow_url_fetch", True)))
if disabled:
self.test_internet_status.setText(tr("tools_admin.internet_disabled"))
self.test_internet_status.setStyleSheet("color: #c00;")
return
def job(worker):
from ..core import tls_trust
ok, message = tls_trust.diagnose_internet()
return {"ok": ok, "message": message}
def done(result):
ok = result.get("ok")
self.test_internet_status.setText(result.get("message", ""))
self.test_internet_status.setStyleSheet("color: #090;" if ok else "color: #c00;")
self.test_internet_btn.setEnabled(True)
def failed(e):
self.test_internet_status.setText(str(e))
self.test_internet_status.setStyleSheet("color: #c00;")
self.test_internet_btn.setEnabled(True)
w = AgentWorker(job)
w.finished_ok.connect(done)
w.failed.connect(failed)
self._net_worker = w # keep a ref so the thread isn't GC'd mid-run
self.test_internet_btn.setEnabled(False)
self.test_internet_status.setStyleSheet("")
self.test_internet_status.setText(tr("settings.testing_internet"))
w.start()
# ---- i18n -----------------------------------------------------------------
def _retranslate(self) -> None:
self.subtabs.setTabText(0, tr("tools_admin.subtab_tool"))
self.subtabs.setTabText(1, tr("tools_admin.subtab_connector"))
self._hint.setText(tr("tools_admin.hint"))
self.test_internet_btn.setText(tr("settings.test_internet"))
self.test_internet_btn.setToolTip(tr("settings.test_internet_tooltip"))
self.refresh_btn.setText(tr("tools_admin.refresh"))
self.table.setHorizontalHeaderLabels([
tr("tools_admin.col_name"), tr("tools_admin.col_desc"),
tr("tools_admin.col_enabled"),
])
self.jira_note.setText(tr("tools_admin.jira_note"))
self.refresh()