From 2ac43a97c1ba2ebf8df992a85a47dff0c5f8f345 Mon Sep 17 00:00:00 2001 From: Anh Tran Nguyen Minh Date: Mon, 7 Sep 2026 19:23:32 +0900 Subject: [PATCH] =?UTF-8?q?fix(ui):=20tab=20Th=C6=B0=20m=E1=BB=A5c=20gi?= =?UTF-8?q?=E1=BB=AF=20=C4=91=C3=BAng=20th=C6=B0=20m=E1=BB=A5c=20ng=C6=B0?= =?UTF-8?q?=E1=BB=9Di=20d=C3=B9ng=20t=E1=BB=B1=20ch=E1=BB=8Dn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hai lỗi riêng biệt cùng gây ra "chọn folder khác, sang tab khác rồi quay lại thì về folder cũ": 1. WorkspaceFileTree.root_changed KHÔNG có ai lắng nghe trong toàn bộ repo. Người dùng chọn thư mục trong cây thì chỉ cái cây đổi gốc; _root, khung xem và terminal ở lại thư mục cũ — ba widget con lệch nhau ngay từ lúc bấm chọn. 2. _load_current gọi set_root(project.workspace_dir()) vô điều kiện, và _goto gọi workspace.refresh() mỗi lần vào lại màn Workspace, nên mỗi cú chuyển tab kéo thư mục về workspace của project. set_project_root() bỏ qua khi project KHÔNG đổi. Đổi sang project khác thì vẫn re-root — thư mục của màn này thuộc về project; chỉ lần refresh trong CÙNG một project là không được đụng. Co-Authored-By: Claude Opus 5 (1M context) --- presentation/folder/folder_tab.py | 30 ++++++ tests/ui/test_folder_root_persists.py | 135 ++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 tests/ui/test_folder_root_persists.py diff --git a/presentation/folder/folder_tab.py b/presentation/folder/folder_tab.py index 2fffcda..bb41775 100644 --- a/presentation/folder/folder_tab.py +++ b/presentation/folder/folder_tab.py @@ -34,6 +34,9 @@ class FolderTab(QWidget): super().__init__() self.ctx = ctx self._root = str(ctx.config.cowork_output_dir()) + # Goc ma project dang chon ap xuong, de phan biet "doi project" voi + # "chi la refresh" — xem set_project_root(). + self._project_root = "" root_layout = QVBoxLayout(self) split = QSplitter(Qt.Horizontal) @@ -82,6 +85,10 @@ class FolderTab(QWidget): self.terminal.expanded.connect(lambda: self.terminal.set_cwd(self._root)) root_layout.addWidget(self.terminal) + # ``root_changed`` truoc day KHONG co ai lang nghe: nguoi dung tu chon + # thu muc trong cay thi chi cai cay doi goc, con khung xem va terminal o + # lai thu muc cu. + self.tree.root_changed.connect(self._on_user_picked_root) self.tree.file_selected.connect(self.preview.open_file) self.preview.status_message.connect(self.status_message.emit) self.ai_panel.status_message.connect(self.status_message.emit) @@ -105,6 +112,29 @@ class FolderTab(QWidget): self.preview.set_root(path) self.terminal.set_cwd(path) + def _on_user_picked_root(self, path: str) -> None: + """Người dùng tự chọn thư mục trong cây: lan sang khung xem và terminal.""" + self._root = path + self.preview.set_root(path) + self.terminal.set_cwd(path) + + def set_project_root(self, path: str) -> None: + """Áp thư mục gốc theo project đang chọn. + + Bỏ qua nếu project KHÔNG đổi. ``WorkspaceTab.refresh()`` — và qua đó + ``_load_current`` — chạy lại mỗi lần người dùng vào lại màn Workspace + (``_goto`` gọi nó), nên gọi ``set_root`` vô điều kiện sẽ kéo thư mục về + workspace của project và xoá mất lựa chọn tay: chọn folder khác, chuyển + tab rồi quay lại là mất. + + Đổi sang project khác thì vẫn re-root — thư mục của màn này thuộc về + project, chỉ có lần refresh trong CÙNG một project là không được đụng. + """ + if path == self._project_root: + return + self._project_root = path + self.set_root(path) + def _toggle_ai_panel(self) -> None: """Gập/mở panel AI-Edit; mở ra thì báo cho panel biết để nó nạp model lần đầu.""" show = self.ai_btn.isChecked() diff --git a/tests/ui/test_folder_root_persists.py b/tests/ui/test_folder_root_persists.py new file mode 100644 index 0000000..1810772 --- /dev/null +++ b/tests/ui/test_folder_root_persists.py @@ -0,0 +1,135 @@ +"""Thư mục người dùng tự chọn ở tab Thư mục không được biến mất khi chuyển tab. + +Hai lỗi riêng biệt cùng gây ra triệu chứng "chọn folder khác, sang tab khác rồi +quay lại thì về folder cũ": + +1. ``WorkspaceFileTree.root_changed`` **không có ai lắng nghe**. Người dùng chọn + thư mục trong cây thì chỉ cái cây đổi gốc; ``_root``, khung xem và terminal ở + lại thư mục cũ. Tín hiệu chết. +2. ``WorkspaceTab._load_current`` gọi ``set_root(project.workspace_dir())`` vô + điều kiện, và ``_goto`` gọi ``workspace.refresh()`` mỗi lần vào lại màn + Workspace — nên mỗi cú chuyển tab kéo thư mục về workspace của project. + +Đổi sang project KHÁC thì vẫn phải re-root: thư mục của màn này thuộc về project. +Chỉ có lần refresh trong cùng một project là không được đụng. +""" +from __future__ import annotations + +import pytest + +pytest.importorskip("PySide6", reason="cần PySide6 để dựng widget thật") + + +class _Cfg: + """Đủ cho FolderTab, không hơn.""" + + def __init__(self, out_dir): + self._out = str(out_dir) + self.data = {} + + def cowork_output_dir(self): + from pathlib import Path + return Path(self._out) + + +class _Ctx: + def __init__(self, out_dir): + self.config = _Cfg(out_dir) + + +@pytest.fixture +def folder_tab(qapp, tmp_path): + from cowork_local.presentation.folder.folder_tab import FolderTab + + goc = tmp_path / "goc-ban-dau" + goc.mkdir() + tab = FolderTab(_Ctx(goc)) + yield tab, tmp_path + tab.deleteLater() + + +# ---- lỗi 1: tín hiệu chết ------------------------------------------------ + +def test_nguoi_dung_chon_thu_muc_thi_khung_xem_va_terminal_theo_kip(folder_tab, tmp_path): + """``root_changed`` phải được nối, không thì ba widget con lệch nhau.""" + tab, base = folder_tab + moi = base / "nguoi-dung-chon" + moi.mkdir() + + tab.tree.set_root(str(moi)) # phát root_changed như khi bấm chọn thư mục + + assert tab._root == str(moi) + assert tab.preview._root == str(moi) + + +# ---- lỗi 2: refresh không được ghi đè lựa chọn tay ---------------------- + +def test_refresh_cung_project_khong_keo_ve_thu_muc_cu(folder_tab, tmp_path): + """Đây chính là triệu chứng: chuyển tab rồi quay lại là mất lựa chọn.""" + tab, base = folder_tab + ws = base / "workspace-cua-project" + ws.mkdir() + tab.set_project_root(str(ws)) # vào màn lần đầu + + nguoi_dung_chon = base / "folder-khac" + nguoi_dung_chon.mkdir() + tab.tree.set_root(str(nguoi_dung_chon)) + + tab.set_project_root(str(ws)) # chuyển tab rồi quay lại -> refresh + + assert tab._root == str(nguoi_dung_chon), "lựa chọn tay bị ghi đè" + + +def test_doi_sang_project_khac_thi_van_re_root(folder_tab, tmp_path): + """Thư mục của màn này thuộc về project — đổi project là phải đổi theo.""" + tab, base = folder_tab + ws_a = base / "ws-a" + ws_b = base / "ws-b" + ws_a.mkdir() + ws_b.mkdir() + + tab.set_project_root(str(ws_a)) + khac = base / "tu-chon" + khac.mkdir() + tab.tree.set_root(str(khac)) + + tab.set_project_root(str(ws_b)) # người dùng đổi project + + assert tab._root == str(ws_b) + + +def test_quay_lai_project_cu_thi_ve_workspace_cua_no(folder_tab, tmp_path): + tab, base = folder_tab + ws_a = base / "ws-a" + ws_b = base / "ws-b" + ws_a.mkdir() + ws_b.mkdir() + + tab.set_project_root(str(ws_a)) + tab.set_project_root(str(ws_b)) + tab.set_project_root(str(ws_a)) + + assert tab._root == str(ws_a) + + +def test_lan_dau_vao_man_van_ap_duoc_goc(folder_tab, tmp_path): + """``_project_root`` khởi tạo rỗng nên lần gọi đầu không bị bỏ qua.""" + tab, base = folder_tab + ws = base / "ws" + ws.mkdir() + + tab.set_project_root(str(ws)) + + assert tab._root == str(ws) + + +# ---- chỗ gọi phải dùng hàm mới ------------------------------------------- + +def test_workspace_tab_goi_set_project_root_chu_khong_set_root(): + from pathlib import Path + + src = (Path(__file__).resolve().parents[2] / "ui" / "workspace_tab.py").read_text(encoding="utf-8") + + assert "_folder.set_project_root(" in src + assert "_folder.set_root(" not in src, ( + "gọi set_root() vô điều kiện là quay lại đúng lỗi cũ")