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.
This commit is contained in:
Hermes Agent
2026-09-24 23:59:07 -05:00
committed by brooklyn!
parent c16e8037d2
commit 953ca9e094
11 changed files with 41 additions and 41 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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<string>(PROJECT_SCOPE_KEY, ALL_PROJECTS, {
decode: raw => raw || ALL_PROJECTS,
encode: value => value || ALL_PROJECTS
})
export function exitProjectScope(): void {
$projectScope.set(ALL_PROJECTS)
}

View File

@@ -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,

View File

@@ -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<string>(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