From 953ca9e094e80061174b8e1716e8781bee8cd356 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 24 Sep 2026 23:59:07 -0500 Subject: [PATCH] refactor(desktop): move project scope into a dependency-light store module Profile and gateway-switch stores need to reset the scope synchronously; importing store/projects from them would close an import cycle. --- .../sidebar/chat-sidebar.integration.test.tsx | 3 ++- apps/desktop/src/app/chat/sidebar/index.tsx | 4 +-- .../hooks/default-new-session.test.tsx | 2 +- .../hooks/use-session-actions.test.tsx | 3 ++- .../hooks/use-session-actions/index.ts | 3 ++- .../src/app/session/new-session-route.ts | 2 +- .../session/workspace-session-target.test.ts | 3 ++- apps/desktop/src/store/coding-status.ts | 10 ++----- apps/desktop/src/store/project-scope.ts | 26 +++++++++++++++++++ apps/desktop/src/store/projects.test.ts | 4 +-- apps/desktop/src/store/projects.ts | 22 +--------------- 11 files changed, 41 insertions(+), 41 deletions(-) create mode 100644 apps/desktop/src/store/project-scope.ts diff --git a/apps/desktop/src/app/chat/sidebar/chat-sidebar.integration.test.tsx b/apps/desktop/src/app/chat/sidebar/chat-sidebar.integration.test.tsx index 978c36f51a..6a86df027f 100644 --- a/apps/desktop/src/app/chat/sidebar/chat-sidebar.integration.test.tsx +++ b/apps/desktop/src/app/chat/sidebar/chat-sidebar.integration.test.tsx @@ -10,7 +10,8 @@ import { registry } from '@/contrib/registry' import { $connectionsRegistry } from '@/store/connection-registry-state' import { $sidebarMessagingOpenIds, setSidebarAgentsGrouped, setSidebarGrouping } from '@/store/layout' import { $activeGatewayProfile, $profiles, setShowAllProfiles } from '@/store/profile' -import { $projectScope, $projectTree, ALL_PROJECTS } from '@/store/projects' +import { $projectScope, ALL_PROJECTS } from '@/store/project-scope' +import { $projectTree } from '@/store/projects' import { $currentCwd, $messagingSessions, diff --git a/apps/desktop/src/app/chat/sidebar/index.tsx b/apps/desktop/src/app/chat/sidebar/index.tsx index 1a696bf6b2..ceec2ebc9e 100644 --- a/apps/desktop/src/app/chat/sidebar/index.tsx +++ b/apps/desktop/src/app/chat/sidebar/index.tsx @@ -86,18 +86,16 @@ import { sidebarProfileForScope } from '@/store/profile' import { $profileRailVisible } from '@/store/profile-rail-prefs' +import { $projectScope, ALL_PROJECTS, exitProjectScope } from '@/store/project-scope' import { $activeProjectId, $newProjectDropPlacement, $projectOwnerBySessionId, $projects, - $projectScope, $projectTree, $projectTreeLoading, $reposScanning, - ALL_PROJECTS, enterProject, - exitProjectScope, followEnteredProjectCwd, openProjectCreate, refreshProjects, diff --git a/apps/desktop/src/app/session/hooks/default-new-session.test.tsx b/apps/desktop/src/app/session/hooks/default-new-session.test.tsx index b96918b037..34524b1068 100644 --- a/apps/desktop/src/app/session/hooks/default-new-session.test.tsx +++ b/apps/desktop/src/app/session/hooks/default-new-session.test.tsx @@ -15,7 +15,7 @@ import { ensureGatewayProfile, resolveNewChatOwnerRoute } from '@/store/profile' -import { $projectScope, ALL_PROJECTS } from '@/store/projects' +import { $projectScope, ALL_PROJECTS } from '@/store/project-scope' import { $activeSessionId, $sessions, diff --git a/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx b/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx index dba07ccf47..ce0717c23f 100644 --- a/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx +++ b/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx @@ -33,7 +33,8 @@ import { ensureGatewayAgent, ensureGatewayProfile } from '@/store/profile' -import { $projectScope, $projectTree, ALL_PROJECTS } from '@/store/projects' +import { $projectScope, ALL_PROJECTS } from '@/store/project-scope' +import { $projectTree } from '@/store/projects' import { $activeSessionId, $activeSessionStoredIdRotation, diff --git a/apps/desktop/src/app/session/hooks/use-session-actions/index.ts b/apps/desktop/src/app/session/hooks/use-session-actions/index.ts index 1aa9bb13af..ca98a4fcf4 100644 --- a/apps/desktop/src/app/session/hooks/use-session-actions/index.ts +++ b/apps/desktop/src/app/session/hooks/use-session-actions/index.ts @@ -55,7 +55,8 @@ import { normalizeProfileKey, resolveNewChatOwnerRoute } from '@/store/profile' -import { $projectScope, resolveNewSessionCwd } from '@/store/projects' +import { $projectScope } from '@/store/project-scope' +import { resolveNewSessionCwd } from '@/store/projects' import { receiveApprovalRequest, replayPendingApproval } from '@/store/prompts' import { clearStoredTranscriptReadOnly, markStoredTranscriptReadOnly } from '@/store/read-only-transcript' import { diff --git a/apps/desktop/src/app/session/new-session-route.ts b/apps/desktop/src/app/session/new-session-route.ts index 9a0c77f43f..f86cab13e9 100644 --- a/apps/desktop/src/app/session/new-session-route.ts +++ b/apps/desktop/src/app/session/new-session-route.ts @@ -10,7 +10,7 @@ import { ensureGatewayProfile, pinLegacyNewChatProfile } from '@/store/profile' -import { $projectScope, ALL_PROJECTS } from '@/store/projects' +import { $projectScope, ALL_PROJECTS } from '@/store/project-scope' import { isPeerInstanceWindow, isProfilePinnedWindow, diff --git a/apps/desktop/src/app/session/workspace-session-target.test.ts b/apps/desktop/src/app/session/workspace-session-target.test.ts index b50f91a61c..0fc0cd911d 100644 --- a/apps/desktop/src/app/session/workspace-session-target.test.ts +++ b/apps/desktop/src/app/session/workspace-session-target.test.ts @@ -1,7 +1,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { $activeGatewayProfile, $newChatProfile } from '@/store/profile' -import { $projectScope, $projectTree, ALL_PROJECTS } from '@/store/projects' +import { $projectScope, ALL_PROJECTS } from '@/store/project-scope' +import { $projectTree } from '@/store/projects' import { $currentBranch, $currentCwd, diff --git a/apps/desktop/src/store/coding-status.ts b/apps/desktop/src/store/coding-status.ts index 4b40bf257e..c5819943c3 100644 --- a/apps/desktop/src/store/coding-status.ts +++ b/apps/desktop/src/store/coding-status.ts @@ -3,14 +3,8 @@ import { atom, computed, type ReadableAtom } from 'nanostores' import type { HermesGitWorktree, HermesRepoStatus } from '@/global' import { desktopGit } from '@/lib/desktop-git' -import { - $projectScope, - $projectTree, - $worktreeDialog, - $worktreeRefreshToken, - ALL_PROJECTS, - projectRootCwd -} from './projects' +import { $projectScope, ALL_PROJECTS } from './project-scope' +import { $projectTree, $worktreeDialog, $worktreeRefreshToken, projectRootCwd } from './projects' import { $busy, $currentCwd, diff --git a/apps/desktop/src/store/project-scope.ts b/apps/desktop/src/store/project-scope.ts new file mode 100644 index 0000000000..a05bbb9ddc --- /dev/null +++ b/apps/desktop/src/store/project-scope.ts @@ -0,0 +1,26 @@ +import { persistentAtom } from '@/lib/persisted' + +// ── Project scope (the "you're inside a project" view, mirroring profile scope)─ +// The sidebar's grouped view is a project switcher: ALL_PROJECTS shows the +// project overview (a list you drill into), and a concrete id means you've +// "entered" that project so only its worktrees/branches/sessions show. This is +// pure view state (localStorage), distinct from the durable active-project +// pointer in projects.db — though entering a project also makes it active so new +// chats land there, exactly as selecting a profile does. +// +// Project ids belong to ONE backend's projects.db, so switching profile or +// connection must leave the scope (store/profile, store/gateway-switch). It lives +// in this dependency-light module so those stores can reset it synchronously, +// before the fresh draft resolves its cwd from it. +export const ALL_PROJECTS = '__all_projects__' + +const PROJECT_SCOPE_KEY = 'hermes.desktop.projectScope' + +export const $projectScope = persistentAtom(PROJECT_SCOPE_KEY, ALL_PROJECTS, { + decode: raw => raw || ALL_PROJECTS, + encode: value => value || ALL_PROJECTS +}) + +export function exitProjectScope(): void { + $projectScope.set(ALL_PROJECTS) +} diff --git a/apps/desktop/src/store/projects.test.ts b/apps/desktop/src/store/projects.test.ts index fa3ab16c15..4a7c4a9a3a 100644 --- a/apps/desktop/src/store/projects.test.ts +++ b/apps/desktop/src/store/projects.test.ts @@ -7,18 +7,16 @@ import { $activeGatewayProfile, $profileScope, ALL_PROFILES, setShowAllProfiles import { $currentCwd, $selectedStoredSessionId, $sessions, applyConfiguredDefaultProjectDir } from '@/store/session' import type { ProjectInfo } from '@/types/hermes' +import { $projectScope, ALL_PROJECTS, exitProjectScope } from './project-scope' import { $activeProjectId, $projects, - $projectScope, $projectsRpcAvailable, $projectTree, addProjectFolder, - ALL_PROJECTS, createProject, deleteProject, enterProject, - exitProjectScope, fetchProjectSessions, openProjectCreate, pickProjectFolder, diff --git a/apps/desktop/src/store/projects.ts b/apps/desktop/src/store/projects.ts index f7b2eb8541..0ed1cd0d16 100644 --- a/apps/desktop/src/store/projects.ts +++ b/apps/desktop/src/store/projects.ts @@ -15,7 +15,6 @@ import { desktopDefaultCwd, isDesktopFsRemoteMode, selectDesktopPaths, writeDesk import { desktopGit } from '@/lib/desktop-git' import { isMissingRestEndpoint, isMissingRpcMethod } from '@/lib/gateway-rpc' import { isUnderPath } from '@/lib/path-compare' -import { persistentAtom } from '@/lib/persisted' import { revealFile } from '@/store/file-actions' import { $gateway, activeGateway, ensureActiveGatewayOpen } from '@/store/gateway' import { $sidebarShowAllSessions, setSidebarAgentsGrouped } from '@/store/layout' @@ -27,6 +26,7 @@ import { normalizeProfileKey, requestFreshSession } from '@/store/profile' +import { $projectScope, ALL_PROJECTS } from '@/store/project-scope' import { $currentCwd, $selectedStoredSessionId, @@ -80,22 +80,6 @@ function projectsStaleBackendError(): Error { // True while the disk scan is in flight (drives the "finding repos" hint). export const $reposScanning = atom(false) -// ── Project scope (the "you're inside a project" view, mirroring profile scope)─ -// The sidebar's grouped view is a project switcher: ALL_PROJECTS shows the -// project overview (a list you drill into), and a concrete id means you've -// "entered" that project so only its worktrees/branches/sessions show. This is -// pure view state (localStorage), distinct from the durable active-project -// pointer in projects.db — though entering a project also makes it active so new -// chats land there, exactly as selecting a profile does. -export const ALL_PROJECTS = '__all_projects__' - -const PROJECT_SCOPE_KEY = 'hermes.desktop.projectScope' - -export const $projectScope = persistentAtom(PROJECT_SCOPE_KEY, ALL_PROJECTS, { - decode: raw => raw || ALL_PROJECTS, - encode: value => value || ALL_PROJECTS -}) - // Enter a project: scope the sidebar to it and make it the active project // (best-effort — the durable pointer is nice-to-have, the view scope is the // point). Never opens a session. @@ -110,10 +94,6 @@ export function enterProject(id: string): void { } } -export function exitProjectScope(): void { - $projectScope.set(ALL_PROJECTS) -} - // A project's working root: its primary folder, else the first repo that has // one. Empty for the path-less Home bucket. (The sidebar's `projectTreeCwd` is // the same rule over the same tree — this is the store-side copy so the store