Files
f9f6bc01fd
CI / test (push) Canceled after 0s
Feature/delta team/epic r04 (#7)
## 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>
2026-08-31 05:15:13 +00:00

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())