fix(graphrag): đổi thư mục project thì quét lại đồ thị theo thư mục mới

Sau khi đường dẫn trên thanh đã trỏ đúng thư mục mới, các node giữa màn vẫn là
của thư mục cũ: không có lệnh quét lại nào được phát ra.

Cùng một họ sai lầm với hai mảnh trước — câu hỏi "có gì đổi không" trả lời bằng
project id chứ không bằng thứ quyết định kết quả quét:

    project_changed = pid != self._active_project_id

Đổi thư mục giữ nguyên id, nên project_changed là False và cả khối phát tín
hiệu lẫn khối gọi _scan() đều bị bỏ qua, trong khi dòng đặt path_edit lại nằm
ngoài khối đó — thanh địa chỉ đúng mà đồ thị đứng yên.

Tách khối "project sandbox lock" sang graph_project_lock.py: graph_renderer.py
đang ở 399/400 dòng, đúng một dòng trước trần của scripts/check_loc.py, và cổng
đó nói rõ cách duy nhất đúng khi chạm trần là tách file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-22 08:39:22 +09:00
co-authored by Claude Opus 5
parent 76225aa118
commit b500b3e57d
3 changed files with 254 additions and 58 deletions
+83
View File
@@ -0,0 +1,83 @@
"""Khoá phạm vi quét của màn GraphRAG vào một project.
Tách khỏi ``graph_renderer.py``: file đó đã ở 399/400 dòng — đúng một dòng
trước trần của ``scripts/check_loc.py``, và cổng ấy nói rõ cách duy nhất đúng
khi chạm trần là tách file, không phải nới con số. Khối này là chỗ tự nhiên
để cắt: ba phương thức dưới đây chỉ nói về một việc — project nào đang khoá,
và thư mục nào đi theo nó — còn phần còn lại của renderer lo việc vẽ.
Là mixin chứ không phải đối tượng rời, cùng lý do như ``NavRailMixin``: ba
phương thức này đọc/ghi state của chính renderer (``project_combo``,
``path_edit``, ``_needs_scan``…). Biến thành đối tượng cộng tác thì phải viết
lại từng chỗ ``self.X`` thành ``self.renderer.X`` mà không đổi hành vi gì.
"""
from __future__ import annotations
from cowork_local.i18n import tr
class GraphProjectLockMixin:
"""Ba phương thức khoá-theo-project. Trộn vào ``GraphRenderer``."""
def _refresh_project_combo(self) -> None:
"""Nạp lại danh sách project vào bộ chọn, giữ nguyên project đang chọn."""
from cowork_local.core.projects import list_projects
keep = self._active_project_id
self.project_combo.blockSignals(True)
self.project_combo.clear()
self.project_combo.addItem(tr("structure.project_none"), "")
row_to_select = 0
for i, p in enumerate(list_projects(), start=1):
self.project_combo.addItem(p.name, p.project_id)
if p.project_id == keep:
row_to_select = i
self.project_combo.setCurrentIndex(row_to_select)
self.project_combo.blockSignals(False)
def set_project(self, project_id: str) -> None:
"""Khoá phạm vi quét vào một project (chuỗi rỗng là bỏ khoá)."""
pid = project_id or ""
self._refresh_project_combo()
target = self.project_combo.findData(pid)
if target < 0:
target = 0
if self.project_combo.currentIndex() == target:
self._on_project_changed(target)
else:
self.project_combo.setCurrentIndex(target)
def _on_project_changed(self, _idx: int) -> None:
"""Áp trạng thái khoá: đường dẫn chuyển sang chỉ đọc và trỏ vào thư mục"""
from cowork_local.core.projects import load_project
pid = self.project_combo.currentData() or ""
project_changed = pid != self._active_project_id
self._active_project_id = pid
locked = bool(pid)
self.path_edit.setReadOnly(locked)
self._pick_btn.setEnabled(not locked)
if locked:
project = load_project(pid)
if project is not None:
self.path_edit.setText(str(project.workspace_dir()))
# Changing the FOLDER changes what we scan just as much as changing the
# project does. Keying this off the id alone left the path in the bar
# updated while the graph in the middle still showed the old folder's
# nodes: "Đổi" in the Project screen moves the folder, never the id.
duong_dan = self.path_edit.text().strip()
doi_muc_tieu = project_changed or duong_dan != self._active_path
self._active_path = duong_dan
if doi_muc_tieu:
self.project_changed.emit() # GraphQaWidget drops its temp extraction cache
# Mark it and scan on the next visit rather than now — see
# auto_scan_and_fit()'s docstring for why.
self._needs_scan = True
# ...except when this screen is the one on show. The picker lives HERE,
# so a user changing project is already looking at the graph: there is
# no "next visit" to defer to, and they had to press Scan by hand.
# Deferring still applies when the change came from the Workspace
# screen while this one is hidden, which is what it was for.
if self.isVisible() and duong_dan:
self._needs_scan = False
self._scan()