CI / test (push) Canceled after 0s
## Summary epic r04 - begin refactor ## Change Type - [x] Cowork feature - [ ] Bug fix - [ ] Core AI contribution - [ ] Test / hardening - [ ] Performance - [ ] Documentation ## Related Work Cowork Task: Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets Core AI Issue: Core Task: Related PR: ## Scope What is intentionally included? What is intentionally NOT included? ## Validation - [ ] Unit tests - [ ] Integration tests - [ ] Manual verification - [ ] Regression check Commands / evidence: ## Security Impact Permission / credential / network / customer data impact: ## Compatibility - [ ] No breaking change - [ ] Breaking change documented ## Reviewer Notes Anything Cowork reviewers should pay attention to. --------- Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com> Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com> Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com> Co-authored-by: Vu Dam Tuan <vudt15@fpt.com> Co-authored-by: Hiep Ha Van <hiephv3@fpt.com> Co-authored-by: Lam Hoang Van <lamhv7@fpt.com> Reviewed-on: #7 Co-authored-by: Duy Le Huu <duylh19@fpt.com>
140 lines
6.3 KiB
Python
140 lines
6.3 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"), "")
|
|
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 _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(self._ROW_WORKSPACE, self.workspace._cowork_tab_idx)
|
|
self.workspace.show_history_pane()
|
|
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())
|