## 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>
This commit was merged in pull request #7.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user