diff --git a/ui/settings_dialog.py b/ui/settings_dialog.py index ea759f6..338b926 100644 --- a/ui/settings_dialog.py +++ b/ui/settings_dialog.py @@ -1,6 +1,16 @@ -"""Settings dialog: AI provider, Sandbox, the unified Connectors (MCP) group -(CAD / CAE / MS365 / Other — MCP servers + REST connectors in one place), -and the merged "Parameter" group (Cowork / Attachments / GraphRAG caps).""" +"""Hộp thoại Cài đặt — khung lắp ráp. + +Năm mục, mỗi mục một trang: Chung, AI Provider, Bảo mật sandbox, Tham số, +Auto Model Routing. Bốn mục đầu... đúng hơn: bốn trong năm mục đã bóc sang +``presentation/settings/`` (R08-T07); file này còn giữ mục Bảo mật sandbox, +phần lắp ráp danh sách mục bên trái, và ``_save`` gọi ``apply_to`` của từng +widget con. + +Không còn phần Connector nào ở đây: nó đã dời sang Monitoring → Tools → +Connector từ trước. Ngày 25/08 dọn nốt 108 dòng MS365 chết còn sót lại của +lần dời đó — năm hàm gọi lẫn nhau, không đường vào, và đọc ba thuộc tính +chưa từng được gán nên gọi vào là AttributeError. +""" from __future__ import annotations from PySide6.QtCore import Qt @@ -8,17 +18,13 @@ from PySide6.QtGui import QGuiApplication from PySide6.QtWidgets import ( QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFormLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, - QMessageBox, QPushButton, QScrollArea, QSpinBox, QTreeWidget, + QMessageBox, QPushButton, QScrollArea, QSpinBox, QTreeWidgetItem, QVBoxLayout, QWidget, ) -from ..core.ext_connectors import CATEGORIES as EXT_CATEGORIES -from ..core.worker import AgentWorker from ..i18n import tr -from ..state import AppContext -from .icons import icon, IconLabel +from .icons import IconLabel from .widgets import ToggleSwitch -from .ext_connector_dialog import ExtConnectorEditDialog from ..presentation.settings.general_settings_widget import GeneralSettingsWidget @@ -133,9 +139,7 @@ class SettingsDialog(QDialog): root.addWidget(self.sandbox_group) # Connectors (MCP / REST API) are managed entirely in Monitoring → Tools - # → Connector now — no connector UI in Settings. (_ms365_workers is kept # for the dead-but-retained MS365 OAuth sign-in handlers below.) - self._ms365_workers = [] # --- Parameter --- # Đã bóc sang presentation/settings/parameter_settings_widget.py (R08-T07). @@ -249,118 +253,10 @@ class SettingsDialog(QDialog): # ---- MS365 zero-config sign-in ("connect like Claude") --------------- - def _refresh_ms365_status(self) -> None: - from ..core.ms365_auth import current_identity - who = current_identity(self.ctx.config) - if who: - self.ms365_status.setText(tr("settings.ms365_signed_in", who=who)) - self.ms365_signin_btn.setEnabled(False) - self.ms365_signout_btn.setEnabled(True) - else: - self.ms365_status.setText(tr("settings.ms365_signed_out")) - self.ms365_signin_btn.setEnabled(True) - self.ms365_signout_btn.setEnabled(False) - self.ms365_signin_btn.setText(tr("settings.ms365_signin_btn")) - self.ms365_signout_btn.setText(tr("settings.ms365_signout_btn")) - def _ms365_sign_in(self) -> None: - from ..core.ms365_auth import current_identity, sign_in - self.ms365_signin_btn.setEnabled(False) - self.ms365_status.setText(tr("settings.ms365_signing_in")) - cfg = self.ctx.config - def job(worker): - # on_code fires (worker thread) with the MSAL device-flow dict — - # marshal it to the UI thread via the worker's event signal. - return sign_in(lambda flow: worker.event.emit({"device_flow": flow}), cfg) - def on_event(ev: dict) -> None: - if "device_flow" in ev: - self._show_ms365_device_code(ev["device_flow"]) - def done(_result) -> None: - self._close_ms365_code_dialog() - self.ctx.save() - self._refresh_ms365_status() - QMessageBox.information( - self, tr("settings.ms365_signin_btn"), - tr("settings.ms365_signed_in", who=current_identity(cfg))) - - def failed(err: str) -> None: - self._close_ms365_code_dialog() - self._refresh_ms365_status() - QMessageBox.warning(self, tr("settings.ms365_signin_btn"), err) - - w = AgentWorker(job) - w.event.connect(on_event) - w.finished_ok.connect(done) - w.failed.connect(failed) - self._ms365_workers.append(w) - w.start() - - def _close_ms365_code_dialog(self) -> None: - dlg = getattr(self, "_ms365_code_dialog", None) - if dlg is not None: - dlg.close() - self._ms365_code_dialog = None - - def _show_ms365_device_code(self, flow: dict) -> None: - """Auto-open the sign-in page + show the one-time code in a COPYABLE, - non-modal dialog (so the worker keeps polling and can auto-close it on - success). The code is also copied to the clipboard immediately.""" - import webbrowser - - code = flow.get("user_code", "") - url = flow.get("verification_uri", "https://microsoft.com/devicelogin") - # Auto-copy the code so the user can just paste it. - QGuiApplication.clipboard().setText(code) - # Auto-open the browser to the (code-prefilled, if available) sign-in page. - try: - webbrowser.open(flow.get("verification_uri_complete") or url) - except Exception: # noqa: BLE001 — a headless box just shows the link to click - pass - - self._close_ms365_code_dialog() - dlg = QDialog(self) - dlg.setWindowTitle(tr("settings.ms365_signin_btn")) - dlg.setMinimumWidth(420) - lay = QVBoxLayout(dlg) - info = QLabel(tr("settings.ms365_code_hint", url=url)) - info.setWordWrap(True) - info.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextBrowserInteraction) - info.setOpenExternalLinks(True) - lay.addWidget(info) - - code_row = QHBoxLayout() - code_edit = QLineEdit(code) - code_edit.setReadOnly(True) - f = code_edit.font() - f.setPointSize(f.pointSize() + 4) - f.setBold(True) - code_edit.setFont(f) - code_edit.setCursorPosition(0) - copy_btn = QPushButton(tr("settings.ms365_copy_code")) - copy_btn.setIcon(icon("document")) - copy_btn.clicked.connect(lambda: QGuiApplication.clipboard().setText(code)) - open_btn = QPushButton(tr("settings.ms365_open_link")) - open_btn.setIcon(icon("link")) - open_btn.clicked.connect(lambda: webbrowser.open(flow.get("verification_uri_complete") or url)) - code_row.addWidget(code_edit, 1) - code_row.addWidget(copy_btn) - code_row.addWidget(open_btn) - lay.addLayout(code_row) - - buttons = QDialogButtonBox(QDialogButtonBox.Close) - buttons.rejected.connect(dlg.reject) - lay.addWidget(buttons) - - self._ms365_code_dialog = dlg - dlg.show() # non-modal — sign-in polling continues; done() closes it - - def _ms365_sign_out(self) -> None: - from ..core.ms365_auth import sign_out_default - sign_out_default(self.ctx.config) - self._refresh_ms365_status()