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
+3 -14
View File
@@ -13,12 +13,10 @@
import React from 'react';
import { resolveProjectForSessionDirectory } from '@/lib/projectResolution';
import { subscribeOpenchamberEvents } from '@/lib/openchamberEvents';
import { useAgentMemoryStore } from '@/stores/useAgentMemoryStore';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useUIStore } from '@/stores/useUIStore';
import { useProjectContextOwner } from '@/hooks/useProjectContextOwner';
/**
* The directory is a parameter rather than read from `useEffectiveDirectory`,
@@ -29,18 +27,9 @@ export const useAgentMemorySync = (directory: string | null): void => {
const enabled = useUIStore((state) => (
state.agentMemoryFeatureAvailable && state.agentMemoryToolEnabled
));
const projects = useProjectsStore((state) => state.projects);
const availableWorktreesByProject = useSessionUIStore((state) => state.availableWorktreesByProject);
const effectiveDirectory = directory ?? '';
const load = useAgentMemoryStore((state) => state.load);
const projectPath = React.useMemo(() => {
if (!effectiveDirectory) {
return null;
}
const resolved = resolveProjectForSessionDirectory(projects, availableWorktreesByProject, effectiveDirectory);
return resolved?.path ?? null;
}, [availableWorktreesByProject, effectiveDirectory, projects]);
const owner = useProjectContextOwner(directory);
const projectPath = owner?.path ?? null;
React.useEffect(() => {
if (!enabled) {
@@ -0,0 +1,48 @@
import { describe, expect, test } from 'bun:test';
import { CHAT_DRAFT_PROJECT_ID } from '@/lib/chatDirectories';
import { resolveProjectContextOwner } from './useProjectContextOwner';
const projects = [
{ id: 'openchamber', path: '/workspace/openchamber', label: 'OpenChamber' },
];
describe('resolveProjectContextOwner', () => {
test('resolves a managed chat directory to the Chats root instead of the active project', () => {
const owner = resolveProjectContextOwner({
projects,
worktreesByProject: new Map(),
directory: '/Users/test/.config/openchamber/chats/2026-08-27/session-a',
activeProjectId: 'openchamber',
chatDraftOpen: false,
chatDraftTarget: 'project',
homeDirectory: '/Users/test',
});
expect(owner).toEqual({
id: CHAT_DRAFT_PROJECT_ID,
path: '/Users/test/.config/openchamber/chats',
});
});
test('resolves a worktree session to its owning project', () => {
const owner = resolveProjectContextOwner({
projects,
worktreesByProject: new Map([
['/workspace/openchamber', [{
path: '/workspace/openchamber-feature',
projectDirectory: '/workspace/openchamber',
branch: 'feature',
label: 'feature',
}]],
]),
directory: '/workspace/openchamber-feature',
activeProjectId: null,
chatDraftOpen: false,
chatDraftTarget: 'project',
homeDirectory: '/Users/test',
});
expect(owner).toEqual({ id: 'openchamber', path: '/workspace/openchamber' });
});
});
@@ -0,0 +1,80 @@
import React from 'react';
import { CHAT_DRAFT_PROJECT_ID, getChatsRootForHome, getChatsRootFromDirectory } from '@/lib/chatDirectories';
import { normalizePath } from '@/lib/pathNormalization';
import { resolveProjectForSessionDirectory } from '@/lib/projectResolution';
import type { ProjectRef } from '@/lib/projectContextApi';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { useSessionUIStore } from '@/sync/session-ui-store';
import type { WorktreeMetadata } from '@/types/worktree';
import type { ProjectEntry } from '@/lib/api/types';
interface ProjectContextOwnerInput {
projects: ProjectEntry[];
worktreesByProject: Map<string, WorktreeMetadata[]>;
directory: string | null;
activeProjectId: string | null;
chatDraftOpen: boolean;
chatDraftTarget: 'chat' | 'project';
homeDirectory: string | null;
}
export const resolveProjectContextOwner = ({
projects,
worktreesByProject,
directory,
activeProjectId,
chatDraftOpen,
chatDraftTarget,
homeDirectory,
}: ProjectContextOwnerInput): ProjectRef | null => {
const chatsRoot = getChatsRootFromDirectory(directory) ?? getChatsRootForHome(homeDirectory);
const normalizedDirectory = normalizePath(directory);
const normalizedChatsRoot = normalizePath(chatsRoot);
const ownsChats = chatDraftOpen
? chatDraftTarget === 'chat'
: Boolean(normalizedDirectory && normalizedChatsRoot && (
normalizedDirectory === normalizedChatsRoot || normalizedDirectory.startsWith(`${normalizedChatsRoot}/`)
));
if (ownsChats && chatsRoot) {
return { id: CHAT_DRAFT_PROJECT_ID, path: chatsRoot };
}
const sessionProject = resolveProjectForSessionDirectory(projects, worktreesByProject, directory);
if (sessionProject) {
return { id: sessionProject.id, path: sessionProject.path };
}
const activeProject = projects.find((project) => project.id === activeProjectId) ?? projects[0] ?? null;
return activeProject ? { id: activeProject.id, path: activeProject.path } : null;
};
/** The single owner used by Project knowledge and agent-memory synchronization. */
export const useProjectContextOwner = (directory: string | null): ProjectRef | null => {
const projects = useProjectsStore((state) => state.projects);
const activeProjectId = useProjectsStore((state) => state.activeProjectId);
const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
const worktreesByProject = useSessionUIStore((state) => state.availableWorktreesByProject);
const chatDraftOpen = useSessionUIStore((state) => state.newSessionDraft.open);
const chatDraftTarget = useSessionUIStore((state) => state.newSessionDraft.target);
return React.useMemo(() => resolveProjectContextOwner({
projects,
worktreesByProject,
directory,
activeProjectId,
chatDraftOpen,
chatDraftTarget,
homeDirectory,
}), [
activeProjectId,
chatDraftOpen,
chatDraftTarget,
directory,
homeDirectory,
projects,
worktreesByProject,
]);
};