Delta team/fix comment ui v2 (#11)
CI / test (push) Canceled after 0s

## Summary

What changed and why?

## Change Type

- [ ] Cowork feature
- [ ] Bug fix
- [ ] Core AI contribution
- [ ] Test / hardening
- [ ] Performance
- [ ] Documentation

## Related Work

Cowork Task:

Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets

Core AI Issue:

Core Task:

Related PR:

## Scope

What is intentionally included?

What is intentionally NOT included?

## Validation

- [ ] Unit tests
- [ ] Integration tests
- [ ] Manual verification
- [ ] Regression check

Commands / evidence:

## Security Impact

Permission / credential / network / customer data impact:

## Compatibility

- [ ] No breaking change
- [ ] Breaking change documented

## Reviewer Notes

Anything Cowork reviewers should pay attention to.

Reviewed-on: #11
Co-authored-by: Duy Le Huu <duylh19@fpt.com>
This commit was merged in pull request #11.
This commit is contained in:
2026-09-14 13:15:40 +00:00
committed by gitea-admin
parent 1b8429e33a
commit b71a622227
17 changed files with 448 additions and 358 deletions
+6 -83
View File
@@ -13,20 +13,17 @@ chưa từng được gán nên gọi vào là AttributeError.
"""
from __future__ import annotations
import secrets
from PySide6.QtCore import Qt
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,
QGroupBox, QHBoxLayout, QListWidget, QListWidgetItem,
QScrollArea, QSpinBox,
QTreeWidgetItem, QVBoxLayout, QWidget,
)
from ..i18n import tr
from .dialog_buttons import dialog_buttons
from .icons import IconLabel
from .widgets import ToggleSwitch
@@ -37,25 +34,6 @@ from ..presentation.settings.routing_settings_widget import RoutingSettingsWidge
from ..presentation.settings.about_widget import AboutSettingsWidget
def _sandbox_password_matches(entered: str, stored: str) -> bool:
"""Whether ``entered`` unlocks the Sandbox Security group.
An empty ``stored`` must never match. ``DEFAULT_CONFIG`` ships
``agent_security.sandbox_pw = ""`` and the config handed to this dialog is
always deep-merged with those defaults, so a plain ``entered == stored``
accepts an empty field on every install that never set a password. The MS365
unlock guards the same way — see ``json_config_repository.unlock_ms365``.
Both sides are compared as UTF-8 bytes, not as ``str``:
``compare_digest`` raises ``TypeError`` on ``str`` holding anything outside
ASCII, and this app defaults to Vietnamese and ships to Japanese customers,
so an accented password is ordinary input rather than an edge case.
"""
if not entered or not stored:
return False
return secrets.compare_digest(entered.encode("utf-8"), stored.encode("utf-8"))
class SettingsDialog(QDialog):
"""Hộp thoại Cài đặt: cột mục lục bên trái, các trang bên phải
(Nhà cung cấp · Connectors · Định tuyến · Tham số · Chung).
@@ -108,29 +86,10 @@ class SettingsDialog(QDialog):
self.sandbox_group = QGroupBox(tr("settings.group.sandbox"))
sbl = QVBoxLayout(self.sandbox_group)
# --- Password protection for Sandbox Security (at top) ---
self.sandbox_pw_label = IconLabel("lock", tr("settings.sandbox_pw_label"))
sbl.addWidget(self.sandbox_pw_label)
pw_row = QHBoxLayout()
self.sandbox_pw_edit = QLineEdit("")
self.sandbox_pw_edit.setPlaceholderText(tr("settings.sandbox_pw_placeholder"))
self.sandbox_pw_edit.setEchoMode(QLineEdit.Password)
pw_row.addWidget(self.sandbox_pw_edit, 1)
self.sandbox_unlock_btn = QPushButton(tr("settings.sandbox_unlock_btn"))
self.sandbox_unlock_btn.clicked.connect(self._sandbox_unlock)
pw_row.addWidget(self.sandbox_unlock_btn)
self.sandbox_locked_status = IconLabel("lock", tr("settings.sandbox_locked"), color="#c00")
self.sandbox_locked_status.text_label().setStyleSheet("color: #c00; font-weight: bold;")
pw_row.addWidget(self.sandbox_locked_status)
sbl.addLayout(pw_row)
self._sandbox_unlocked = False # Start LOCKED — must enter password first
self._sandbox_pw = sec.get("sandbox_pw", "")
# Separator line between pw section and sandbox settings
pw_sep = QLabel("────────────────")
sbl.addWidget(pw_sep)
# Nhóm này KHÔNG còn khoá bằng mật khẩu: bốn công tắc dưới đây bật/tắt
# tự do. Khoá cũ chỉ là rào chống bấm nhầm ở phía giao diện, không phải
# cơ chế bảo mật thật (rào thật nằm ở sandbox lúc chạy lệnh), nên bỏ đi
# theo yêu cầu thay vì giữ một bước nhập mật khẩu không bảo vệ được gì.
self.sandbox_confirm = ToggleSwitch(tr("settings.sandbox_confirm_commands"))
self.sandbox_confirm.setChecked(bool(sec.get("cowork_confirm_commands", False)))
self.sandbox_confirm.setToolTip(tr("settings.sandbox_confirm_commands_tooltip"))
@@ -160,14 +119,6 @@ class SettingsDialog(QDialog):
# Resource limits (CPU/Memory/Disk I/O) moved to the Parameter group
# below — see _param_section("settings.group.sandbox_limits").
# Collect all sandbox-editable widgets and lock them until unlocked
self._sandbox_widgets = [
self.sandbox_confirm, self.sandbox_block_network,
self.ai_check, self.sec_enabled,
]
for _w in self._sandbox_widgets:
_w.setEnabled(False)
root.addWidget(self.sandbox_group)
# Connectors (MCP / REST API) are managed entirely in Monitoring → Tools
@@ -299,34 +250,6 @@ class SettingsDialog(QDialog):
def _sandbox_unlock(self) -> None:
"""Mở khoá nhóm cài đặt sandbox bằng mật khẩu.
Đây là khoá phía giao diện để chặn bấm nhầm vào một mục nhạy cảm, KHÔNG
phải cơ chế bảo mật thật.
"""
pw = self.sandbox_pw_edit.text()
if not self._sandbox_pw:
# No password configured. Refusing with "wrong password" would be a
# dead end — the user would keep retrying a password that cannot
# exist — so name the actual state instead.
QMessageBox.warning(self, tr("settings.sandbox_pw_unset_title"),
tr("settings.sandbox_pw_unset_body"))
return
if _sandbox_password_matches(pw, self._sandbox_pw):
self._sandbox_unlocked = True
self.sandbox_locked_status.setText(tr("settings.sandbox_unlocked"))
self.sandbox_locked_status.set_icon("unlock", "#090")
self.sandbox_locked_status.text_label().setStyleSheet("color: #090; font-weight: bold;")
# Enable all sandbox widgets
for w in self._sandbox_widgets:
w.setEnabled(True)
QMessageBox.information(self, tr("settings.group.sandbox"),
tr("settings.sandbox_unlocked_body"))
else:
QMessageBox.warning(self, tr("settings.sandbox_pw_wrong_title"),
tr("settings.sandbox_pw_wrong_body"))
def _save(self) -> None:
"""Gom cấu hình từ mọi trang con rồi ghi xuống đĩa."""
data = self.ctx.config.data
+11 -7
View File
@@ -63,10 +63,9 @@ class WorkspaceTab(ProjectEditingMixin, QWidget):
"""(label, index, icon_name, enabled) for EVERY sub-tab, hidden ones
included.
The rail lists all five all the time and greys out the ones the project
gate is currently closing (Cowork, GraphRAG) instead of removing them —
same gate, shown rather than hidden, so the menu stops changing shape
under the user's hand. See nav_subtabs() for the visible-only view.
Cột ``enabled`` là trạng thái cổng project; ``NavRailMixin._rebuild_nav``
bỏ hẳn những hàng đang đóng (Cowork, GraphRAG) khỏi menu trái cho tới khi
người dùng chọn một project. See nav_subtabs() for the visible-only view.
"""
icons = {self._project_tab_idx: "folder", self._cowork_tab_idx: "chat",
self._co4e_tab_idx: "flow", self._folder_tab_idx: "folder",
@@ -78,8 +77,8 @@ class WorkspaceTab(ProjectEditingMixin, QWidget):
def subtab_available(self, index: int) -> bool:
"""False while the project gate is holding this sub-tab shut.
The rail greys those rows out, but that only guards the rail. This lets
every other route ask the same question of the same state.
Rail bỏ hẳn những hàng đó khỏi menu, nhưng đó chỉ chắn được đường vào
qua rail. Hàm này để mọi đường vào khác hỏi cùng một trạng thái.
"""
return bool(0 <= index < self.tabs.count() and self.tabs.isTabVisible(index))
@@ -610,7 +609,12 @@ class WorkspaceTab(ProjectEditingMixin, QWidget):
counts = self._project_counts()
self.project_list.blockSignals(True)
self.project_list.clear()
row_to_select = 0
# -1 chứ không phải 0: chưa chọn gì thì KHÔNG tự chọn hộ project đầu
# danh sách. Chọn hộ là mở luôn cổng Cowork/GraphRAG (xem
# _update_tab_visibility) cho một project người dùng chưa hề bấm vào —
# lúc mở app, và cả sau khi xoá project đang mở. Có ``keep`` khớp thì
# vẫn giữ đúng dòng cũ như trước.
row_to_select = -1
for i, p in enumerate(list_projects()):
chats, tasks = counts.get(p.project_id, (0, 0))
# No text on the item: the row widget paints the name, and setting