feat(ui): màn quản lý project — chế độ chỉ-xem, chặn trùng tên, menu chuột phải
Bốn thay đổi trên cùng một khối, gom vào presentation/workspace/project_editing.py: - Project đã có mở ra ở dạng CHỈ ĐỌC; phải bấm "Sửa project" (nút vàng) mới gõ được, và nút Lưu chuyển xanh lá. Trước đây form luôn mở nên chỉ cần lỡ tay là sửa mất nội dung của một project đang dùng mà không có gì cảnh báo. - Chặn TRÙNG TÊN khi tạo và khi lưu (không phân biệt hoa thường, bỏ khoảng trắng hai đầu). Danh sách bên trái chỉ hiện tên, nên trùng tên là không phân biệt được nữa. Chặn TRƯỚC khi ghi đĩa. - Chuột phải một project: Mở / Sửa / Xoá. Trước đây bấm phải không làm gì cả. - workspace.counts được format một lần lúc dựng dòng nên đổi ngôn ngữ xong nó vẫn nằm ở ngôn ngữ cũ, kể cả khi chọn tiếng Anh. ProjectRow giờ giữ SỐ và tự format lại — KHÔNG gọi refresh(), vì refresh() kết thúc bằng _load_current() và sẽ xoá mất nội dung người dùng đang gõ dở. Ba nút về chung một hàng với "Project mới", và cùng theo một luật hiện/ẩn: chỉ hiện trên sub-tab Project và khi có project đang mở. Hàng tiêu đề vắt ngang cả màn Workspace nên thứ gì đặt lên đó cũng lọt sang Cowork/Co4E/Thư mục/GraphRAG nếu không tự ẩn. Màu vàng/xanh lá đi qua token warning/success. Hai nút dùng nền *_soft với chữ và viền là token, KHÔNG phải nền đặc: hai token đó được chỉnh làm màu CHỮ (cùng lý do accent và accent_solid là hai token riêng) — chữ trắng trên warning dark #CCA700 chỉ đạt ~2.3:1, trượt AA. ui/workspace_tab.py đang ở đúng trần bánh cóc nên phần mã mới bắt buộc ra module riêng; chuyển _ProjectRow và CRUD sang đó làm file co từ 566 xuống 523 dòng mã, và bánh cóc được siết theo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
-75
@@ -21,38 +21,19 @@ from __future__ import annotations
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QFileDialog, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem,
|
||||
QMenu, QMessageBox, QPlainTextEdit, QPushButton, QSplitter, QTabWidget,
|
||||
QPlainTextEdit, QPushButton, QSplitter, QTabWidget,
|
||||
QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from ..i18n import on_language_changed, tr
|
||||
from ..presentation.workspace.project_editing import ProjectEditingMixin, ProjectRow
|
||||
from ..state import AppContext
|
||||
from .icons import collapse_left_icon, icon
|
||||
from .osutil import open_folder
|
||||
from .widgets import CollapseStrip
|
||||
|
||||
|
||||
class _ProjectRow(QWidget):
|
||||
"""A project in the list: its name, and under it how much is in it.
|
||||
|
||||
The drawing gives every row a second line — "2 đoạn chat · 3 task" — which
|
||||
is the only thing on this screen that says a project holds anything at all.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, counts: str):
|
||||
"""Một dòng dự án trong danh sách: tên ở trên, số liệu tóm tắt ở dưới."""
|
||||
super().__init__()
|
||||
lay = QVBoxLayout(self)
|
||||
lay.setContentsMargins(6, 4, 6, 4)
|
||||
lay.setSpacing(0)
|
||||
title = QLabel(name)
|
||||
sub = QLabel(counts)
|
||||
sub.setObjectName("hint")
|
||||
lay.addWidget(title)
|
||||
lay.addWidget(sub)
|
||||
|
||||
|
||||
class WorkspaceTab(QWidget):
|
||||
class WorkspaceTab(ProjectEditingMixin, QWidget):
|
||||
"""Trang chủ Workspace: cột project, cột lịch sử, và 5 sub-tab
|
||||
(Dự án · Cowork · Co4E · Thư mục · GraphRAG).
|
||||
|
||||
@@ -265,6 +246,7 @@ class WorkspaceTab(QWidget):
|
||||
self._apply_pane_visibility()
|
||||
|
||||
self.refresh()
|
||||
self.install_project_editing()
|
||||
on_language_changed(self._retranslate)
|
||||
self._retranslate()
|
||||
|
||||
@@ -495,6 +477,7 @@ class WorkspaceTab(QWidget):
|
||||
self._browse_btn.setToolTip(tr("workspace.browse_tooltip"))
|
||||
self._open_btn.setText(tr("workspace.open_folder"))
|
||||
self._save_btn.setText(tr("workspace.save"))
|
||||
self.retranslate_project_rows()
|
||||
self._proj_collapse_btn.setToolTip(tr("workspace.collapse_projects_tooltip"))
|
||||
self._projects_strip.setToolTip(tr("workspace.expand_projects_tooltip"))
|
||||
self.tabs.setTabText(self._project_tab_idx, tr("workspace.tab_project"))
|
||||
@@ -612,7 +595,7 @@ class WorkspaceTab(QWidget):
|
||||
if p.description:
|
||||
item.setToolTip(p.description)
|
||||
self.project_list.addItem(item)
|
||||
row = _ProjectRow(p.name, tr("workspace.counts", chats=chats, tasks=tasks))
|
||||
row = ProjectRow(p.name, chats, tasks)
|
||||
item.setSizeHint(row.sizeHint())
|
||||
self.project_list.setItemWidget(item, row)
|
||||
if p.project_id == keep:
|
||||
@@ -627,11 +610,11 @@ class WorkspaceTab(QWidget):
|
||||
@staticmethod
|
||||
def _project_counts():
|
||||
"""{project_id: (chats, tasks)} — read once per refresh, not per row."""
|
||||
from ..core.history import list_conversations
|
||||
from ..core.history import history_dirs, list_conversations_by_project
|
||||
from ..core.tasks import list_tasks
|
||||
|
||||
out: dict = {}
|
||||
for conv in list_conversations():
|
||||
for conv in list_conversations_by_project(history_dirs()):
|
||||
pid = conv.get("project_id") or "default"
|
||||
chats, tasks = out.get(pid, (0, 0))
|
||||
out[pid] = (chats + 1, tasks)
|
||||
@@ -660,7 +643,7 @@ class WorkspaceTab(QWidget):
|
||||
|
||||
def _load_current(self) -> None:
|
||||
"""Nạp project đang chọn lên form và nối mọi sub-tab vào nó."""
|
||||
from ..core.projects import load_project
|
||||
from ..core.projects import load_project, project_history_dir
|
||||
|
||||
pid = self._selected_id()
|
||||
self._current_id = pid
|
||||
@@ -674,7 +657,7 @@ class WorkspaceTab(QWidget):
|
||||
# sharing that folder shares the history (another machine can view +
|
||||
# continue). No project → global history dir (attribute cleared).
|
||||
if project is not None:
|
||||
self.ctx.config._project_history_dir = project.workspace_dir() / ".cowork_history"
|
||||
self.ctx.config._project_history_dir = project_history_dir(project)
|
||||
else:
|
||||
self.ctx.config._project_history_dir = None
|
||||
if project is not None:
|
||||
@@ -685,7 +668,7 @@ class WorkspaceTab(QWidget):
|
||||
self._del_btn.setEnabled(True)
|
||||
self._reload_threads()
|
||||
if getattr(self, "_folder", None) is not None:
|
||||
self._folder.set_root(str(project.workspace_dir()))
|
||||
self._folder.set_project_root(str(project.workspace_dir()))
|
||||
else:
|
||||
self.name_edit.clear()
|
||||
self.desc_edit.clear()
|
||||
@@ -823,7 +806,7 @@ class WorkspaceTab(QWidget):
|
||||
self.ctx.active_project_id = pid or "default"
|
||||
self._refresh_mode_toggles()
|
||||
if self._structure is not None:
|
||||
self._structure.set_project(pid)
|
||||
self._structure.set_workspace_project(pid)
|
||||
if self._sidebar is not None:
|
||||
self._sidebar.set_project_filter(pid) # "" → show all (no project selected)
|
||||
# Route Co4E flow output into THIS project's workspace folder (so flow
|
||||
@@ -876,51 +859,6 @@ class WorkspaceTab(QWidget):
|
||||
self.threads.addTopLevelItem(item)
|
||||
|
||||
# ---- actions -----------------------------------------------------------
|
||||
def _create(self) -> None:
|
||||
"""Tạo project mới với tên mặc định rồi chọn nó."""
|
||||
from ..core.projects import new_project
|
||||
|
||||
project = new_project(tr("workspace.default_new_name"))
|
||||
self._current_id = project.project_id
|
||||
self.refresh()
|
||||
self.projects_changed.emit()
|
||||
self.name_edit.setFocus()
|
||||
self.name_edit.selectAll()
|
||||
|
||||
def _delete(self) -> None:
|
||||
"""Xoá project đang chọn sau khi hỏi xác nhận."""
|
||||
from ..core.projects import delete_project, load_project
|
||||
|
||||
pid = self._selected_id()
|
||||
project = load_project(pid) if pid else None
|
||||
if project is None:
|
||||
return
|
||||
if QMessageBox.question(
|
||||
self, tr("workspace.delete"),
|
||||
tr("workspace.delete_confirm", name=project.name)) != QMessageBox.Yes:
|
||||
return
|
||||
delete_project(pid)
|
||||
self._current_id = ""
|
||||
self.refresh() # empty workspace → Cowork/GraphRAG hidden until a new project
|
||||
self.projects_changed.emit()
|
||||
self.status_message.emit(tr("workspace.deleted", name=project.name))
|
||||
|
||||
def _save(self) -> None:
|
||||
"""Lưu tên, mô tả và chỉ dẫn chung của project đang mở."""
|
||||
from ..core.projects import load_project, save_project
|
||||
|
||||
pid = self._current_id
|
||||
project = load_project(pid) if pid else None
|
||||
if project is None:
|
||||
return
|
||||
project.name = self.name_edit.text().strip() or project.name
|
||||
project.description = self.desc_edit.text().strip()
|
||||
project.instructions = self.instr_edit.toPlainText().strip()
|
||||
save_project(project)
|
||||
self.refresh()
|
||||
self.projects_changed.emit()
|
||||
self.status_message.emit(tr("workspace.saved", name=project.name))
|
||||
|
||||
def _pick_folder(self) -> None:
|
||||
"""Chọn thư mục sandbox cho project đang mở."""
|
||||
from ..core.projects import load_project, save_project
|
||||
@@ -940,7 +878,7 @@ class WorkspaceTab(QWidget):
|
||||
|
||||
def _open_workspace(self) -> None:
|
||||
"""Mở thư mục sandbox của project trong trình quản lý tệp của hệ điều hành."""
|
||||
from ..core.projects import load_project
|
||||
from ..core.projects import load_project, project_history_dir
|
||||
|
||||
project = load_project(self._current_id) if self._current_id else None
|
||||
if project is None:
|
||||
|
||||
Reference in New Issue
Block a user