fix(ui): scope agent memory to project context owner

This commit is contained in:
Bohdan Triapitsyn
2026-08-27 20:18:12 +03:00
parent 7aae5a6634
commit 03f4b5e3e0
9 changed files with 214 additions and 54 deletions
@@ -6,46 +6,26 @@ import { useProjectsStore } from '@/stores/useProjectsStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { formatDirectoryName } from '@/lib/utils';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { CHAT_DRAFT_PROJECT_ID, getChatsRootForHome, getChatsRootFromDirectory, isChatDirectoryPath } from '@/lib/chatDirectories';
import { CHAT_DRAFT_PROJECT_ID } from '@/lib/chatDirectories';
import { useI18n } from '@/lib/i18n';
import { useProjectContextOwner } from '@/hooks/useProjectContextOwner';
export const ProjectContextPanel: React.FC<{
onActionComplete?: () => void;
onOpenPlan?: (plan: { id: string; title: string }) => void;
}> = ({ onActionComplete, onOpenPlan }) => {
const activeProjectId = useProjectsStore((state) => state.activeProjectId);
const projects = useProjectsStore((state) => state.projects);
const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
const { t } = useI18n();
const gitDirectories = useGitStore((state) => state.directories);
const isChatContext = useSessionUIStore((state) => (
state.newSessionDraft.open
? state.newSessionDraft.target === 'chat'
: isChatDirectoryPath(state.currentSessionDirectory)
));
const chatSessionDirectory = useSessionUIStore((state) => state.currentSessionDirectory);
const chatsRoot = getChatsRootFromDirectory(chatSessionDirectory) ?? getChatsRootForHome(homeDirectory);
const projectRef = useProjectContextOwner(chatSessionDirectory);
const isChatContext = projectRef?.id === CHAT_DRAFT_PROJECT_ID;
const activeProject = React.useMemo(() => {
if (isChatContext) return null;
if (activeProjectId) {
return projects.find((project) => project.id === activeProjectId) ?? projects[0] ?? null;
}
return projects[0] ?? null;
}, [activeProjectId, isChatContext, projects]);
const projectRef = React.useMemo(() => {
if (isChatContext && chatsRoot) {
return { id: CHAT_DRAFT_PROJECT_ID, path: chatsRoot };
}
if (!activeProject) {
return null;
}
return {
id: activeProject.id,
path: activeProject.path,
};
}, [activeProject, chatsRoot, isChatContext]);
return projects.find((project) => project.id === projectRef?.id) ?? null;
}, [isChatContext, projectRef?.id, projects]);
const projectLabel = React.useMemo(() => {
if (isChatContext) return t('sessions.sidebar.activity.chatsTitle');
@@ -106,10 +106,16 @@ its own tool. It feeds this panel only — what a session is told about memory i
decided server-side by `packages/web/server/lib/session-knowledge`, so it
reaches sessions that have no UI at all and survives compaction.
Both sides resolve a worktree to its project before touching the store — the
client through `resolveProjectForSessionDirectory`, the server through
`agent-memory/project-resolution`. Keying by the session directory instead filed
a worktree's memories under a project nothing reads.
`useProjectContextOwner` is the client authority shared by this panel and the
memory sync. It resolves managed chat directories to the Chats root and a
worktree to its project before either consumer touches a store. The server uses
`agent-memory/project-resolution` for the same worktree rule. Keying by a
worktree session directory would file memories under a project nothing reads.
Project memory is rendered only when the store's `projectPath` matches the
panel owner. An owner switch hides the previous project's entries before the
new request starts. A failed request marks the new owner unavailable instead of
presenting that hidden list as authoritative empty memory.
Turning the switch back on re-reads the store only after the setting has
finished being written. The switch flips the client immediately, which makes the
@@ -11,7 +11,7 @@ import { useI18n } from '@/lib/i18n';
import { AGENT_MEMORY_BODY_MAX_LENGTH, AGENT_MEMORY_TITLE_MAX_LENGTH, type AgentMemoryEntry, type AgentMemoryScope } from '@/lib/agentMemoryApi';
import { classifyMemory, memoryViewKey, type MemoryBadge } from '@/lib/agentMemoryBadges';
import { cn } from '@/lib/utils';
import { useAgentMemoryStore } from '@/stores/useAgentMemoryStore';
import { selectProjectMemoryForPath, useAgentMemoryStore } from '@/stores/useAgentMemoryStore';
import { useUIStore } from '@/stores/useUIStore';
/**
@@ -160,7 +160,7 @@ export const MemorySection: React.FC<{
const [expandedId, setExpandedId] = React.useState<string | null>(null);
const globalEntries = useAgentMemoryStore((state) => state.global);
const projectEntries = useAgentMemoryStore((state) => state.project);
const projectEntries = useAgentMemoryStore((state) => selectProjectMemoryForPath(state, projectPath));
const globalFailed = useAgentMemoryStore((state) => state.globalFailed);
const projectFailed = useAgentMemoryStore((state) => state.projectFailed);
const deleteEntry = useAgentMemoryStore((state) => state.deleteEntry);
@@ -7,7 +7,7 @@ import { Input } from '@/components/ui/input';
import { useI18n } from '@/lib/i18n';
import { resolveProjectContextId, type ProjectRef, type ProjectTodoItem } from '@/lib/projectContextApi';
import { cn } from '@/lib/utils';
import { useAgentMemoryStore } from '@/stores/useAgentMemoryStore';
import { selectProjectMemoryForPath, useAgentMemoryStore } from '@/stores/useAgentMemoryStore';
import { countHighlightedMemories, memoryViewKey } from '@/lib/agentMemoryBadges';
import { EMPTY_PROJECT_CONTEXT_ENTRY, useProjectContextStore } from '@/stores/useProjectContextStore';
import { useUIStore } from '@/stores/useUIStore';
@@ -133,7 +133,9 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
const memoryDisabledByServer = useAgentMemoryStore((state) => state.disabled);
const memoryVisible = memoryEnabled && !memoryDisabledByServer;
const globalMemory = useAgentMemoryStore((state) => state.global);
const projectMemory = useAgentMemoryStore((state) => state.project);
const projectMemory = useAgentMemoryStore(
(state) => selectProjectMemoryForPath(state, projectRef?.path ?? null),
);
const isMobile = useUIStore((state) => state.isMobile);
const storedTab = useUIStore((state) => state.projectContextTab);