Mở app lên là hai mục đã hiện sẵn dù người dùng chưa bấm chọn project nào.
Cổng này có hai mặt và cả hai đều hở:
1. refresh() mặc định row_to_select = 0, nên lúc mở app (_current_id rỗng)
danh sách tự chọn hộ project đầu tiên -> _update_tab_visibility(True).
Cùng đường đó còn làm app nhảy sang project khác ngay sau khi xoá project
đang mở. Đổi mặc định thành -1; có keep khớp thì vẫn giữ đúng dòng cũ.
2. NavRailMixin._rebuild_nav dựng hàng bị cổng đóng ở dạng mờ kèm tooltip
("shown instead of hidden") nên chúng vẫn nằm trên menu trái. Nay bỏ hẳn
hàng cho tới khi chọn project.
Mặt 1 làm selected_project_id() trả rỗng, khiến bộ chọn project trên rail rơi
về mục 0 và chỉ vào project đầu danh sách trong khi cổng vẫn đóng — hai chỗ
nói hai đằng. Thêm mục "Chọn project…" (khoá app.nav.pick_project, khác
no_project: đã có project, chỉ chưa chọn) để rail nói đúng trạng thái.
tools/check_nav.py và một bài trong test_startup_landing.py đang chốt thiết
kế cũ; cập nhật để chốt hành vi mới. Bài startup giữ nguyên từng assert, chỉ
đổi cách mở cổng từ cửa sau setTabVisible sang chọn project thật.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
175 lines
8.5 KiB
Python
175 lines
8.5 KiB
Python
"""Bộ chọn project và danh sách RECENTS trên thanh rail — R08-T10.
|
|
|
|
Tách khỏi ``nav_rail.py``: thanh rail có hai phần đời sống khác hẳn nhau.
|
|
|
|
Phần điểm đến (Dashboard, Workspace, Giám sát…) là **tĩnh** — dựng một lần,
|
|
đổi khi đổi ngôn ngữ. Phần này thì **động**: đổi mỗi lần người dùng chọn
|
|
project khác, mỗi lần một cuộc trò chuyện được tạo hay kết thúc.
|
|
|
|
Trộn chung một file thì mỗi lần sửa danh sách gần đây lại phải cuộn qua toàn
|
|
bộ phần dựng rail. Cùng kiểu mixin, xem ghi chú ở đầu ``nav_rail.py``.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from PySide6.QtCore import Qt
|
|
from PySide6.QtGui import QColor
|
|
from PySide6.QtWidgets import QTreeWidget, QTreeWidgetItem
|
|
from ...i18n import tr
|
|
from ...ui.widgets import tidy_popup
|
|
from ...theme import current_palette
|
|
|
|
|
|
|
|
class RailProjectMixin:
|
|
"""Bộ chọn project + RECENTS. Trộn vào MainWindow."""
|
|
|
|
def _sync_rail_project(self, *_a) -> None:
|
|
"""Mirror the workspace's project list/selection into the rail picker.
|
|
|
|
One-way on purpose: the project list stays the source of truth, this is
|
|
only a second place to see and change it.
|
|
"""
|
|
if self._syncing_rail_project:
|
|
return
|
|
self._syncing_rail_project = True
|
|
try:
|
|
choices = self.workspace.project_choices()
|
|
current = self.workspace.selected_project_id()
|
|
self.nav_project.clear()
|
|
for name, pid in choices:
|
|
self.nav_project.addItem(f"📁 {name}", pid)
|
|
if not choices:
|
|
# No project yet: say so, and say what to do about it, instead of
|
|
# leaving an empty box and a button that silently does nothing.
|
|
self.nav_project.addItem(tr("app.nav.no_project"), "")
|
|
elif not current:
|
|
# Có project nhưng CHƯA chọn cái nào (mở app lên, hoặc vừa xoá
|
|
# project đang mở). Không có mục này thì combo rơi về mục 0 và
|
|
# chỉ bừa vào project đầu danh sách, trong khi cổng
|
|
# Cowork/GraphRAG vẫn đóng — hai chỗ nói hai đằng.
|
|
self.nav_project.insertItem(0, tr("app.nav.pick_project"), "")
|
|
idx = self.nav_project.findData(current)
|
|
if idx >= 0:
|
|
self.nav_project.setCurrentIndex(idx)
|
|
has = bool(choices)
|
|
tidy_popup(self.nav_project)
|
|
self.nav_project.setEnabled(has)
|
|
self.nav_project_btn.setEnabled(has)
|
|
self.nav_project_btn.setToolTip(
|
|
self.nav_project.currentText().replace("📁 ", "")
|
|
if has else tr("app.nav.create_project_first"))
|
|
self.nav_new_chat.setEnabled(has)
|
|
self.nav_new_chat.setToolTip(
|
|
"" if has else tr("app.nav.create_project_first"))
|
|
finally:
|
|
self._syncing_rail_project = False
|
|
def _fill_rail_project_menu(self) -> None:
|
|
"""Mirror the picker's items. Choosing one moves the picker, which runs
|
|
_on_rail_project_pick — the collapsed rail adds no second code path."""
|
|
menu = self.nav_project_btn.menu()
|
|
menu.clear()
|
|
for i in range(self.nav_project.count()):
|
|
act = menu.addAction(self.nav_project.itemText(i))
|
|
act.setCheckable(True)
|
|
act.setChecked(i == self.nav_project.currentIndex())
|
|
act.triggered.connect(
|
|
lambda _checked=False, row=i: self.nav_project.setCurrentIndex(row))
|
|
def _on_rail_project_pick(self, _idx: int) -> None:
|
|
"""Chọn project ở thanh menu: chuyển workspace sang project đó.
|
|
|
|
Cờ ``_syncing_rail_project`` chặn vòng lặp khi chính workspace vừa đặt lại
|
|
bộ chọn này.
|
|
"""
|
|
if self._syncing_rail_project:
|
|
return
|
|
pid = self.nav_project.currentData()
|
|
if pid:
|
|
self.workspace.choose_project(pid)
|
|
def _refresh_rail_recents(self) -> None:
|
|
"""Re-fill the rail's recents from the active project's history."""
|
|
from ...ui.icons import DOT_BLUE, dot_icon
|
|
from ...ui.icons import icon as _icon
|
|
|
|
tree = self.nav_recents
|
|
blocked = tree.blockSignals(True)
|
|
tree.clear()
|
|
running = self._running_session_ids()
|
|
threads = self.workspace.recent_threads(self._RAIL_RECENTS)
|
|
for t in threads:
|
|
it = QTreeWidgetItem([t["title"]])
|
|
it.setToolTip(0, t["title"])
|
|
if t["session_id"] in running:
|
|
it.setIcon(0, dot_icon(DOT_BLUE)) # same marker as History
|
|
elif t["pinned"]:
|
|
it.setIcon(0, _icon("pin"))
|
|
it.setData(0, Qt.UserRole, {"path": t["path"], "kind": t["kind"]})
|
|
tree.addTopLevelItem(it)
|
|
if not threads:
|
|
it = QTreeWidgetItem([tr("sidebar.empty")])
|
|
it.setDisabled(True)
|
|
tree.addTopLevelItem(it)
|
|
# The way back to everything the rail cannot show — styled as a link
|
|
# (italic, accent-colored) so it reads as "go elsewhere", not another row.
|
|
more = QTreeWidgetItem([tr("app.nav.all_projects")])
|
|
more.setData(0, Qt.UserRole, {"all": True})
|
|
more_font = more.font(0)
|
|
more_font.setItalic(True)
|
|
more.setFont(0, more_font)
|
|
more.setForeground(0, QColor(current_palette().accent))
|
|
tree.addTopLevelItem(more)
|
|
tree.blockSignals(blocked)
|
|
self.nav_recents_hdr.setVisible(not self._nav_collapsed)
|
|
self.nav_recents.setVisible(not self._nav_collapsed)
|
|
def goto_all_projects(self) -> None:
|
|
"""Mở khung "Tất cả project…": Workspace ▸ Cowork, bảng lịch sử bung ra.
|
|
|
|
Phải đi qua ``_goto``, không được gọi thẳng ``show_history_pane()``:
|
|
``_goto`` là chỗ duy nhất dời vệt sáng trên thanh menu theo nội dung.
|
|
Gọi tắt thì nội dung sang Cowork còn vệt sáng ở lại Project — và vì
|
|
``QTreeWidget`` không phát ``currentItemChanged`` khi bấm lại đúng dòng
|
|
đang chọn, bấm "Project" sẽ KHÔNG có tác dụng gì cho tới khi người dùng
|
|
bấm sang mục khác rồi bấm về.
|
|
|
|
Gom vào một hàm vì có hai đường vào: link "Tất cả project…" ở mục GẦN
|
|
ĐÂY, và màn hình mặc định lúc mở app.
|
|
"""
|
|
sub = self.workspace._cowork_tab_idx
|
|
# Chua chon project thi cong project dang giu sub-tab Cowork dong.
|
|
# show_history_pane() mo no bang cua sau, bo qua cong — the la noi dung
|
|
# o Cowork trong khi ca cong lan vet sang deu noi la Project. Man hinh
|
|
# dung cho nguoi chua co project chinh la man quan ly project.
|
|
if sub < 0 or not self.workspace.subtab_available(sub):
|
|
self._goto(self._ROW_WORKSPACE, self.workspace._project_tab_idx)
|
|
return
|
|
self._goto(self._ROW_WORKSPACE, sub)
|
|
self.workspace.show_history_pane()
|
|
# Bang lich su duoc nhung trong sub-tab Cowork cua MOT project, nen
|
|
# WorkspaceTab._bind_project da dat set_project_filter(pid) — no chi hien
|
|
# thread cua project dang chon. Vao day bang link "Tat ca project…" ma van
|
|
# con bo loc thi nguoi dung tao 5 project chi thay 1. Xoa bo loc SAU
|
|
# show_history_pane(), vi ham do di qua duong doi sub-tab.
|
|
self.sidebar.set_project_filter("")
|
|
|
|
def _on_rail_recent(self, item, _col: int = 0) -> None:
|
|
"""Bấm một dòng trong mục GẦN ĐÂY: mở hội thoại đó, hoặc mở khung "Tất cả project…"."""
|
|
data = item.data(0, Qt.UserRole) or {}
|
|
if data.get("all"):
|
|
self.goto_all_projects()
|
|
return
|
|
path = data.get("path")
|
|
if path:
|
|
self._goto(self._ROW_WORKSPACE, self.workspace._cowork_tab_idx)
|
|
self.workspace.open_thread(path, data.get("kind", "cowork"))
|
|
def _on_rail_new_chat(self) -> None:
|
|
"""Start a new chat, from any screen.
|
|
|
|
Same call the Cowork toolbar button makes — that button stays exactly
|
|
where it was; this is a second entry point, not a replacement.
|
|
"""
|
|
self._goto(self._ROW_WORKSPACE, None)
|
|
self.workspace.start_new_chat()
|
|
self._select_nav_row(self._ROW_WORKSPACE, self.workspace.current_subtab())
|
|
def _nav_new_chat_enabled(self) -> bool:
|
|
"""Nút "chat mới" chỉ bật khi đã có ít nhất một project."""
|
|
return bool(self.workspace.project_choices())
|