Rework Workspace ▸ Project to its wireframe

Section 4 draws a "Quản lý project" title with + Project mới on its right, a
caps PROJECT heading over the list, every row carrying "N đoạn chat · M task",
and the form reading Tên / Mô tả / Instructions / Thư mục làm việc. The screen
had none of that: the header still said "Workspace — Projects", the create
button sat at the foot of the list among the list's own controls, rows were
bare names, the folder field had no label, and Instructions carried a
parenthetical the drawing does not.

The row counts are the substantive part — they are the only thing on the screen
that says a project contains anything. Read once per refresh from
list_conversations() and list_tasks() grouped by project_id, not per row.

Delete stays under the list it acts on. The drawing does not show it, but it
does not show it moved either, and dropping a control is not something a layout
pass gets to do.

Also shortened by the drawing: "Đổi thư mục…"/"Mở thư mục" to "Đổi"/"Mở", which
the new "Thư mục làm việc" label above them now disambiguates.

check_project_screen covers the four points and fails when the count line is
removed.

22/22 checkers pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nam Pham Dinh Thanh
2026-08-18 15:12:02 +09:00
co-authored by Claude Opus 5
parent 416d88d72f
commit 9393fc1748
3 changed files with 190 additions and 12 deletions
+65 -8
View File
@@ -32,6 +32,25 @@ 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):
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):
status_message = Signal(str)
open_chat = Signal(str, dict) # kind, conversation — open a thread in Cowork
@@ -98,12 +117,23 @@ class WorkspaceTab(QWidget):
self._sidebar = sidebar
root = QVBoxLayout(self)
# Title row — the drawing puts "+ Project mới" up here beside the title,
# not at the foot of the project list where it read as belonging to the
# list's own controls.
self._header = QLabel()
self._header.setStyleSheet("font-weight:700; font-size:15px;")
self._new_btn = QPushButton()
self._new_btn.setIcon(icon("plus"))
self._new_btn.setObjectName("primary")
self._new_btn.clicked.connect(self._create)
title_row = QHBoxLayout()
title_row.addWidget(self._header)
title_row.addStretch(1)
title_row.addWidget(self._new_btn)
root.addLayout(title_row)
self._hint = QLabel()
self._hint.setObjectName("hint")
self._hint.setWordWrap(True)
root.addWidget(self._header)
root.addWidget(self._hint)
self._split = QSplitter(Qt.Horizontal)
@@ -115,6 +145,9 @@ class WorkspaceTab(QWidget):
ll = QVBoxLayout(left)
ll.setContentsMargins(0, 0, 0, 0)
left_hdr = QHBoxLayout()
self._projects_hdr = QLabel()
self._projects_hdr.setObjectName("navSectionHdr")
left_hdr.addWidget(self._projects_hdr)
self._proj_collapse_btn = QPushButton()
self._proj_collapse_btn.setIcon(collapse_left_icon())
self._proj_collapse_btn.setFixedWidth(28)
@@ -126,15 +159,13 @@ class WorkspaceTab(QWidget):
self.project_list.currentItemChanged.connect(self._on_select)
ll.addWidget(self.project_list, 1)
btns = QHBoxLayout()
self._new_btn = QPushButton()
self._new_btn.setIcon(icon("plus"))
self._new_btn.setObjectName("primary")
self._new_btn.clicked.connect(self._create)
# Delete stays under the list it acts on. The drawing does not show it,
# but it does not show it moved either, and dropping a control is not
# something a layout pass gets to do.
self._del_btn = QPushButton()
self._del_btn.setIcon(icon("trash"))
self._del_btn.clicked.connect(self._delete)
btns.addWidget(self._new_btn, 1)
btns.addWidget(self._del_btn)
btns.addWidget(self._del_btn, 1)
ll.addLayout(btns)
self._projects_panel = left
@@ -240,6 +271,8 @@ class WorkspaceTab(QWidget):
rl.addWidget(self._instr_lbl)
rl.addWidget(self.instr_edit)
self._folder_hdr = QLabel()
rl.addWidget(self._folder_hdr)
folder_row = QHBoxLayout()
self.folder_lbl = QLabel()
self.folder_lbl.setObjectName("hint")
@@ -394,6 +427,8 @@ class WorkspaceTab(QWidget):
def _retranslate(self) -> None:
self._header.setText(tr("workspace.header"))
self._hint.setText(tr("workspace.hint"))
self._projects_hdr.setText(tr("workspace.projects_heading").upper())
self._folder_hdr.setText(tr("workspace.folder_label"))
self._new_btn.setText(tr("workspace.new_project"))
self._del_btn.setText(tr("workspace.delete"))
self._name_lbl.setText(tr("workspace.name"))
@@ -491,21 +526,43 @@ class WorkspaceTab(QWidget):
from ..core.projects import list_projects
keep = self._current_id
counts = self._project_counts()
self.project_list.blockSignals(True)
self.project_list.clear()
row_to_select = 0
for i, p in enumerate(list_projects()):
item = QListWidgetItem(p.name)
chats, tasks = counts.get(p.project_id, (0, 0))
item = QListWidgetItem()
item.setData(Qt.UserRole, p.project_id)
if p.description:
item.setToolTip(p.description)
self.project_list.addItem(item)
row = _ProjectRow(p.name, tr("workspace.counts", chats=chats, tasks=tasks))
item.setSizeHint(row.sizeHint())
self.project_list.setItemWidget(item, row)
if p.project_id == keep:
row_to_select = i
self.project_list.blockSignals(False)
self.project_list.setCurrentRow(row_to_select)
self._load_current()
@staticmethod
def _project_counts():
"""{project_id: (chats, tasks)} — read once per refresh, not per row."""
from ..core.history import list_conversations
from ..core.tasks import list_tasks
out: dict = {}
for conv in list_conversations():
pid = conv.get("project_id") or "default"
chats, tasks = out.get(pid, (0, 0))
out[pid] = (chats + 1, tasks)
for task in list_tasks():
pid = task.get("project_id") or "default"
chats, tasks = out.get(pid, (0, 0))
out[pid] = (chats, tasks + 1)
return out
def _selected_id(self) -> str:
item = self.project_list.currentItem()
return item.data(Qt.UserRole) if item else ""