Enforce the project gate in _goto, not only on the rail rows

Showing Cowork and GraphRAG always, greyed, instead of letting them appear and
vanish, moved the gate from "the row is absent" to "the row is disabled". That
guards the rail — Qt will not select a disabled row — but it guards only the
rail, and _goto is also reached from a RECENTS click and from startup restore.
Called with a gated sub-tab it opened the page while the workspace's own tab
strip still had it hidden.

_goto now asks WorkspaceTab.subtab_available() — the same isTabVisible state
that greys the row — and shows "Chọn project trước" in the status bar instead
of navigating. One gate, one place, whatever triggered the jump.

check_project_gate runs against an EMPTY home, because with any project on disk
the gate is open and the test proves nothing. It asserts the gated rows are
disabled and say why, that clicking them does nothing, that _goto refuses them,
and that + new chat and the project picker are off. Dropping the _goto guard
fails it with "_goto mo duoc 'Cowork' trong khi cong dang dong".

17/17 checkers pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nam Pham Dinh Thanh
2026-08-18 12:06:40 +09:00
co-authored by Claude Opus 5
parent d37a0c1578
commit 16406f863f
3 changed files with 132 additions and 1 deletions
+8 -1
View File
@@ -1074,7 +1074,14 @@ class MainWindow(QMainWindow):
self.workspace.refresh() # re-list projects + threads on entry
widget = self._page_widgets[page]
if sub is not None and hasattr(widget, "select_subtab"):
widget.select_subtab(sub)
# Enforce the project gate here rather than at each entry point. A
# greyed rail row cannot be clicked, but _goto is also reached from
# RECENTS and from startup restore, and it used to open a sub-tab
# the gate was holding shut — page shown, tab strip still hiding it.
if hasattr(widget, "subtab_available") and not widget.subtab_available(sub):
self.statusBar().showMessage(tr("app.nav.needs_project"), 4000)
else:
widget.select_subtab(sub)
# Move the highlight with the content, however navigation was triggered —
# a programmatic _goto used to leave it on whatever was clicked last.
if not self._nav_building: